psbt: put fake xpubs into globals. keystores handle xfp/der_prefix missing

This commit is contained in:
SomberNight 2019-11-01 20:33:53 +01:00
parent 7eb7eb8674
commit e6c841d05f
No known key found for this signature in database
GPG key ID: B33B5F232C6271E9
10 changed files with 195 additions and 95 deletions

View file

@ -394,7 +394,7 @@ class BaseWizard(Logger):
# For segwit, a custom path is used, as there is no standard at all. # For segwit, a custom path is used, as there is no standard at all.
default_choice_idx = 2 default_choice_idx = 2
choices = [ choices = [
('standard', 'legacy multisig (p2sh)', "m/45'/0"), ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")),
('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')),
] ]

View file

@ -3,7 +3,7 @@
# file LICENCE or http://www.opensource.org/licenses/mit-license.php # file LICENCE or http://www.opensource.org/licenses/mit-license.php
import hashlib import hashlib
from typing import List, Tuple, NamedTuple, Union, Iterable from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional
from .util import bfh, bh2u, BitcoinException from .util import bfh, bh2u, BitcoinException
from . import constants from . import constants
@ -335,7 +335,7 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
return path return path
def convert_bip32_intpath_to_strpath(path: List[int]) -> str: def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str:
s = "m/" s = "m/"
for child_index in path: for child_index in path:
if not isinstance(child_index, int): if not isinstance(child_index, int):
@ -363,8 +363,28 @@ def is_bip32_derivation(s: str) -> bool:
return True return True
def normalize_bip32_derivation(s: str) -> str: def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]:
if s is None:
return None
if not is_bip32_derivation(s): if not is_bip32_derivation(s):
raise ValueError(f"invalid bip32 derivation: {s}") raise ValueError(f"invalid bip32 derivation: {s}")
ints = convert_bip32_path_to_list_of_uint32(s) ints = convert_bip32_path_to_list_of_uint32(s)
return convert_bip32_intpath_to_strpath(ints) return convert_bip32_intpath_to_strpath(ints)
def root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional[str]]:
"""Returns the root bip32 fingerprint and the derivation path from the
root to the given xkey, if they can be determined. Otherwise (None, None).
"""
node = BIP32Node.from_xkey(xkey)
derivation_prefix = None
root_fingerprint = None
assert node.depth >= 0, node.depth
if node.depth == 0:
derivation_prefix = 'm'
root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower()
elif node.depth == 1:
child_number_int = int.from_bytes(node.child_number, 'big')
derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])
root_fingerprint = node.fingerprint.hex()
return root_fingerprint, derivation_prefix

View file

@ -446,28 +446,39 @@ class JsonDB(Logger):
self.put('seed_version', 19) self.put('seed_version', 19)
def _convert_version_20(self): def _convert_version_20(self):
# store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores # store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores.
# store explicit None values if we cannot retroactively determine them
if not self._is_upgrade_method_needed(19, 19): if not self._is_upgrade_method_needed(19, 19):
return return
from .bip32 import BIP32Node from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath
# note: This upgrade method reimplements bip32.root_fp_and_der_prefix_from_xkey.
# This is done deliberately, to avoid introducing that method as a dependency to this upgrade.
for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]): for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]):
ks = self.get(ks_name, None) ks = self.get(ks_name, None)
if ks is None: continue if ks is None: continue
xpub = ks.get('xpub', None) xpub = ks.get('xpub', None)
if xpub is None: continue if xpub is None: continue
bip32node = BIP32Node.from_xkey(xpub)
# derivation prefix # derivation prefix
derivation_prefix = ks.get('derivation', 'm') derivation_prefix = ks.get('derivation', None)
ks['derivation'] = derivation_prefix if derivation_prefix is None:
assert bip32node.depth >= 0, bip32node.depth
if bip32node.depth == 0:
derivation_prefix = 'm'
elif bip32node.depth == 1:
child_number_int = int.from_bytes(bip32node.child_number, 'big')
derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int])
ks['derivation'] = derivation_prefix
# root fingerprint # root fingerprint
root_fingerprint = ks.get('ckcc_xfp', None) root_fingerprint = ks.get('ckcc_xfp', None)
if root_fingerprint is not None: if root_fingerprint is not None:
root_fingerprint = root_fingerprint.to_bytes(4, byteorder="little", signed=False).hex().lower() root_fingerprint = root_fingerprint.to_bytes(4, byteorder="little", signed=False).hex().lower()
if root_fingerprint is None: if root_fingerprint is None:
# if we don't have prior data, we set it to the fp of the xpub if bip32node.depth == 0:
# EVEN IF there was already a derivation prefix saved different than 'm' root_fingerprint = bip32node.calc_fingerprint_of_this_node().hex().lower()
node = BIP32Node.from_xkey(xpub) elif bip32node.depth == 1:
root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower() root_fingerprint = bip32node.fingerprint.hex()
ks['root_fingerprint'] = root_fingerprint ks['root_fingerprint'] = root_fingerprint
ks.pop('ckcc_xfp', None) ks.pop('ckcc_xfp', None)
self.put(ks_name, ks) self.put(ks_name, ks)

View file

@ -26,12 +26,14 @@
from unicodedata import normalize from unicodedata import normalize
import hashlib import hashlib
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List import re
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple
from . import bitcoin, ecc, constants, bip32 from . import bitcoin, ecc, constants, bip32
from .bitcoin import deserialize_privkey, serialize_privkey from .bitcoin import deserialize_privkey, serialize_privkey
from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME,
is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation) is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation,
convert_bip32_intpath_to_strpath)
from .ecc import string_to_number, number_to_string from .ecc import string_to_number, number_to_string
from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST,
SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160) SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160)
@ -49,6 +51,7 @@ class KeyStore(Logger):
def __init__(self): def __init__(self):
Logger.__init__(self) Logger.__init__(self)
self.is_requesting_to_be_rewritten_to_wallet_file = False # type: bool
def has_seed(self): def has_seed(self):
return False return False
@ -325,30 +328,60 @@ class Xpub:
self.xpub_receive = None self.xpub_receive = None
self.xpub_change = None self.xpub_change = None
# if these are None now, then it is the responsibility of the caller to # "key origin" info (subclass should persist these):
# also call self.add_derivation_prefix_and_root_fingerprint: self._derivation_prefix = derivation_prefix # type: Optional[str]
self._derivation_prefix = derivation_prefix # note: subclass should persist this self._root_fingerprint = root_fingerprint # type: Optional[str]
self._root_fingerprint = root_fingerprint # note: subclass should persist this
def get_master_public_key(self): def get_master_public_key(self):
return self.xpub return self.xpub
def get_derivation_prefix(self) -> str: def get_derivation_prefix(self) -> Optional[str]:
"""Returns to bip32 path from some root node to self.xpub""" """Returns to bip32 path from some root node to self.xpub
assert self._derivation_prefix is not None, 'derivation_prefix should have been set already' Note that the return value might be None; if it is unknown.
"""
return self._derivation_prefix return self._derivation_prefix
def get_root_fingerprint(self) -> str: def get_root_fingerprint(self) -> Optional[str]:
"""Returns the bip32 fingerprint of the top level node. """Returns the bip32 fingerprint of the top level node.
This top level node is the node at the beginning of the derivation prefix, This top level node is the node at the beginning of the derivation prefix,
i.e. applying the derivation prefix to it will result self.xpub i.e. applying the derivation prefix to it will result self.xpub
Note that the return value might be None; if it is unknown.
""" """
assert self._root_fingerprint is not None, 'root_fingerprint should have been set already'
return self._root_fingerprint return self._root_fingerprint
def add_derivation_prefix_and_root_fingerprint(self, *, derivation_prefix: str, root_node: BIP32Node): def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int]) -> Tuple[bytes, Sequence[int]]:
"""Returns fingerprint and 'full' derivation path corresponding to a derivation suffix.
The fingerprint is either the root fp or the intermediate fp, depending on what is available,
and the 'full' derivation path is adjusted accordingly.
"""
fingerprint_hex = self.get_root_fingerprint()
der_prefix_str = self.get_derivation_prefix()
if fingerprint_hex is not None and der_prefix_str is not None:
# use root fp, and true full path
fingerprint_bytes = bfh(fingerprint_hex)
der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str)
else:
# use intermediate fp, and claim der suffix is the full path
fingerprint_bytes = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node()
der_prefix_ints = convert_bip32_path_to_list_of_uint32('m')
der_full = der_prefix_ints + list(der_suffix)
return fingerprint_bytes, der_full
def get_xpub_to_be_used_in_partial_tx(self) -> str:
assert self.xpub
fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[])
bip32node = BIP32Node.from_xkey(self.xpub)
depth = len(der_full)
child_number_int = der_full[-1] if len(der_full) >= 1 else 0
child_number_bytes = child_number_int.to_bytes(length=4, byteorder="big")
fingerprint = bytes(4) if depth == 0 else bip32node.fingerprint
bip32node = bip32node._replace(depth=depth,
fingerprint=fingerprint,
child_number=child_number_bytes)
return bip32node.to_xpub()
def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node):
assert self.xpub assert self.xpub
derivation_prefix = normalize_bip32_derivation(derivation_prefix)
# try to derive ourselves from what we were given # try to derive ourselves from what we were given
child_node1 = root_node.subkey_at_private_derivation(derivation_prefix) child_node1 = root_node.subkey_at_private_derivation(derivation_prefix)
child_pubkey_bytes1 = child_node1.eckey.get_public_key_bytes(compressed=True) child_pubkey_bytes1 = child_node1.eckey.get_public_key_bytes(compressed=True)
@ -356,14 +389,13 @@ class Xpub:
child_pubkey_bytes2 = child_node2.eckey.get_public_key_bytes(compressed=True) child_pubkey_bytes2 = child_node2.eckey.get_public_key_bytes(compressed=True)
if child_pubkey_bytes1 != child_pubkey_bytes2: if child_pubkey_bytes1 != child_pubkey_bytes2:
raise Exception("(xpub, derivation_prefix, root_node) inconsistency") raise Exception("(xpub, derivation_prefix, root_node) inconsistency")
# store self.add_key_origin(derivation_prefix=derivation_prefix,
self._root_fingerprint = root_node.calc_fingerprint_of_this_node().hex().lower() root_fingerprint=root_node.calc_fingerprint_of_this_node().hex().lower())
self._derivation_prefix = derivation_prefix
def reset_derivation_prefix(self): def add_key_origin(self, *, derivation_prefix: Optional[str], root_fingerprint: Optional[str]):
assert self.xpub assert self.xpub
self._derivation_prefix = 'm' self._root_fingerprint = root_fingerprint
self._root_fingerprint = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node().hex().lower() self._derivation_prefix = normalize_bip32_derivation(derivation_prefix)
def derive_pubkey(self, for_change, n) -> str: def derive_pubkey(self, for_change, n) -> str:
for_change = int(for_change) for_change = int(for_change)
@ -431,20 +463,22 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
def is_watching_only(self): def is_watching_only(self):
return self.xprv is None return self.xprv is None
def add_xpub(self, xpub, *, default_der_prefix=True): def add_xpub(self, xpub):
assert is_xpub(xpub)
self.xpub = xpub self.xpub = xpub
if default_der_prefix: root_fingerprint, derivation_prefix = bip32.root_fp_and_der_prefix_from_xkey(xpub)
self.reset_derivation_prefix() self.add_key_origin(derivation_prefix=derivation_prefix, root_fingerprint=root_fingerprint)
def add_xprv(self, xprv, *, default_der_prefix=True): def add_xprv(self, xprv):
assert is_xprv(xprv)
self.xprv = xprv self.xprv = xprv
self.add_xpub(bip32.xpub_from_xprv(xprv), default_der_prefix=default_der_prefix) self.add_xpub(bip32.xpub_from_xprv(xprv))
def add_xprv_from_seed(self, bip32_seed, xtype, derivation): def add_xprv_from_seed(self, bip32_seed, xtype, derivation):
rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype) rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype)
node = rootnode.subkey_at_private_derivation(derivation) node = rootnode.subkey_at_private_derivation(derivation)
self.add_xprv(node.to_xprv(), default_der_prefix=False) self.add_xprv(node.to_xprv())
self.add_derivation_prefix_and_root_fingerprint(derivation_prefix=derivation, root_node=rootnode) self.add_key_origin_from_root_node(derivation_prefix=derivation, root_node=rootnode)
def get_private_key(self, sequence, password): def get_private_key(self, sequence, password):
xprv = self.get_master_private_key(password) xprv = self.get_master_private_key(password)
@ -568,6 +602,15 @@ class Old_KeyStore(Deterministic_KeyStore):
self._root_fingerprint = xfp.hex().lower() self._root_fingerprint = xfp.hex().lower()
return self._root_fingerprint return self._root_fingerprint
# TODO Old_KeyStore and Xpub could share a common baseclass?
def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int]) -> Tuple[bytes, Sequence[int]]:
fingerprint_hex = self.get_root_fingerprint()
der_prefix_str = self.get_derivation_prefix()
fingerprint_bytes = bfh(fingerprint_hex)
der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str)
der_full = der_prefix_ints + list(der_suffix)
return fingerprint_bytes, der_full
def update_password(self, old_password, new_password): def update_password(self, old_password, new_password):
self.check_password(old_password) self.check_password(old_password)
if new_password == '': if new_password == '':
@ -658,6 +701,14 @@ class Hardware_KeyStore(KeyStore, Xpub):
def ready_to_sign(self): def ready_to_sign(self):
return super().ready_to_sign() and self.has_usable_connection_with_device() return super().ready_to_sign() and self.has_usable_connection_with_device()
def opportunistically_fill_in_missing_info_from_device(self, client):
assert client is not None
if self._root_fingerprint is None:
root_xpub = client.get_xpub('m', xtype='standard')
root_fingerprint = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower()
self._root_fingerprint = root_fingerprint
self.is_requesting_to_be_rewritten_to_wallet_file = True
def bip39_normalize_passphrase(passphrase): def bip39_normalize_passphrase(passphrase):
return normalize('NFKD', passphrase or '') return normalize('NFKD', passphrase or '')
@ -718,16 +769,17 @@ PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES)
def xtype_from_derivation(derivation: str) -> str: 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'"):
return 'p2wpkh'
elif derivation.startswith("m/49'"):
return 'p2wpkh-p2sh'
elif derivation.startswith("m/44'"):
return 'standard'
elif derivation.startswith("m/45'"):
return 'standard'
bip32_indices = convert_bip32_path_to_list_of_uint32(derivation) bip32_indices = convert_bip32_path_to_list_of_uint32(derivation)
if len(bip32_indices) >= 1:
if bip32_indices[0] == 84 + BIP32_PRIME:
return 'p2wpkh'
elif bip32_indices[0] == 49 + BIP32_PRIME:
return 'p2wpkh-p2sh'
elif bip32_indices[0] == 44 + BIP32_PRIME:
return 'standard'
elif bip32_indices[0] == 45 + BIP32_PRIME:
return 'standard'
if len(bip32_indices) >= 4: if len(bip32_indices) >= 4:
if bip32_indices[0] == 48 + BIP32_PRIME: if bip32_indices[0] == 48 + BIP32_PRIME:
# m / purpose' / coin_type' / account' / script_type' / change / address_index # m / purpose' / coin_type' / account' / script_type' / change / address_index
@ -770,7 +822,7 @@ def load_keystore(storage, name) -> KeyStore:
def is_old_mpk(mpk: str) -> bool: def is_old_mpk(mpk: str) -> bool:
try: try:
int(mpk, 16) int(mpk, 16) # test if hex string
except: except:
return False return False
if len(mpk) != 128: if len(mpk) != 128:
@ -804,16 +856,18 @@ def is_private_key_list(text, *, allow_spaces_inside_key=True, raise_on_error=Fa
raise_on_error=raise_on_error)) raise_on_error=raise_on_error))
is_mpk = lambda x: is_old_mpk(x) or is_xpub(x) def is_master_key(x):
is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x) return is_old_mpk(x) or is_bip32_key(x)
is_master_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x)
is_private_key = lambda x: is_xprv(x) or is_private_key_list(x)
is_bip32_key = lambda x: is_xprv(x) or is_xpub(x) def is_bip32_key(x):
return is_xprv(x) or is_xpub(x)
def bip44_derivation(account_id, bip43_purpose=44): 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)) der = "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id))
return normalize_bip32_derivation(der)
def purpose48_derivation(account_id: int, xtype: str) -> str: def purpose48_derivation(account_id: int, xtype: str) -> str:
@ -824,7 +878,8 @@ def purpose48_derivation(account_id: int, xtype: str) -> str:
script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype) script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype)
if script_type_int is None: if script_type_int is None:
raise Exception('unknown xtype: {}'.format(xtype)) raise Exception('unknown xtype: {}'.format(xtype))
return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) der = "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int)
return normalize_bip32_derivation(der)
def from_seed(seed, passphrase, is_p2sh=False): def from_seed(seed, passphrase, is_p2sh=False):

View file

@ -39,6 +39,7 @@ from .logging import get_logger, Logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .plugins.hw_wallet import HW_PluginBase from .plugins.hw_wallet import HW_PluginBase
from .keystore import Hardware_KeyStore
_logger = get_logger(__name__) _logger = get_logger(__name__)
@ -442,7 +443,7 @@ class DeviceMgr(ThreadJob):
self.scan_devices() self.scan_devices()
return self.client_lookup(id_) return self.client_lookup(id_)
def client_for_keystore(self, plugin, handler, keystore, force_pair): def client_for_keystore(self, plugin, handler, keystore: 'Hardware_KeyStore', force_pair):
self.logger.info("getting client for keystore") self.logger.info("getting client for keystore")
if handler is None: if handler is None:
raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing.")) raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing."))
@ -450,12 +451,15 @@ class DeviceMgr(ThreadJob):
devices = self.scan_devices() devices = self.scan_devices()
xpub = keystore.xpub xpub = keystore.xpub
derivation = keystore.get_derivation_prefix() derivation = keystore.get_derivation_prefix()
assert derivation is not None
client = self.client_by_xpub(plugin, xpub, handler, devices) client = self.client_by_xpub(plugin, xpub, handler, devices)
if client is None and force_pair: if client is None and force_pair:
info = self.select_device(plugin, handler, keystore, devices) info = self.select_device(plugin, handler, keystore, devices)
client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices) client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices)
if client: if client:
handler.update_status(True) handler.update_status(True)
if client:
keystore.opportunistically_fill_in_missing_info_from_device(client)
self.logger.info("end client for keystore") self.logger.info("end client for keystore")
return client return client

View file

@ -266,13 +266,18 @@ class Coldcard_KeyStore(Hardware_KeyStore):
d['ckcc_xpub'] = self.ckcc_xpub d['ckcc_xpub'] = self.ckcc_xpub
return d return d
def get_xfp_int(self) -> int:
xfp = self.get_root_fingerprint()
assert xfp is not None
return xfp_int_from_xfp_bytes(bfh(xfp))
def get_client(self): def get_client(self):
# called when user tries to do something like view address, sign somthing. # called when user tries to do something like view address, sign somthing.
# - not called during probing/setup # - not called during probing/setup
# - will fail if indicated device can't produce the xpub (at derivation) expected # - will fail if indicated device can't produce the xpub (at derivation) expected
rv = self.plugin.get_client(self) rv = self.plugin.get_client(self)
if rv: if rv:
xfp_int = xfp_int_for_keystore(self) xfp_int = self.get_xfp_int()
rv.verify_connection(xfp_int, self.ckcc_xpub) rv.verify_connection(xfp_int, self.ckcc_xpub)
return rv return rv
@ -363,7 +368,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
client = self.get_client() client = self.get_client()
assert client.dev.master_fingerprint == xfp_int_for_keystore(self) assert client.dev.master_fingerprint == self.get_xfp_int()
raw_psbt = tx.serialize_as_bytes() raw_psbt = tx.serialize_as_bytes()
@ -570,9 +575,11 @@ class ColdcardPlugin(HW_PluginBase):
xpubs = [] xpubs = []
derivs = set() derivs = set()
for xpub, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): for xpub, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
der_prefix = ks.get_derivation_prefix() fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[])
xpubs.append( (ks.get_root_fingerprint(), xpub, der_prefix) ) fp_hex = fp_bytes.hex().upper()
derivs.add(der_prefix) der_prefix_str = bip32.convert_bip32_intpath_to_strpath(der_full)
xpubs.append( (fp_hex, xpub, der_prefix_str) )
derivs.add(der_prefix_str)
# Derivation doesn't matter too much to the Coldcard, since it # Derivation doesn't matter too much to the Coldcard, since it
# uses key path data from PSBT or USB request as needed. However, # uses key path data from PSBT or USB request as needed. However,
@ -613,10 +620,9 @@ class ColdcardPlugin(HW_PluginBase):
xfp_paths = [] xfp_paths = []
for pubkey_hex in pubkey_deriv_info: for pubkey_hex in pubkey_deriv_info:
ks, der_suffix = pubkey_deriv_info[pubkey_hex] ks, der_suffix = pubkey_deriv_info[pubkey_hex]
xfp_int = xfp_int_for_keystore(ks) fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix)
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix()) xfp_int = xfp_int_from_xfp_bytes(fp_bytes)
der_full = der_prefix + list(der_suffix) xfp_paths.append([xfp_int] + list(der_full))
xfp_paths.append([xfp_int] + der_full)
script = bfh(wallet.pubkeys_to_scriptcode(pubkeys)) script = bfh(wallet.pubkeys_to_scriptcode(pubkeys))
@ -627,9 +633,8 @@ class ColdcardPlugin(HW_PluginBase):
return return
def xfp_int_for_keystore(keystore: Xpub) -> int: def xfp_int_from_xfp_bytes(fp_bytes: bytes) -> int:
xfp = keystore.get_root_fingerprint() return int.from_bytes(fp_bytes, byteorder="little", signed=False)
return int.from_bytes(bfh(xfp), byteorder="little", signed=False)
def xfp2str(xfp: int) -> str: def xfp2str(xfp: int) -> str:

View file

@ -72,6 +72,9 @@ class HW_PluginBase(BasePlugin):
""" """
raise NotImplementedError() raise NotImplementedError()
def get_client(self, keystore: 'Hardware_KeyStore', force_pair: bool = True):
raise NotImplementedError()
def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None): def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None):
pass # implemented in child classes pass # implemented in child classes

View file

@ -772,7 +772,7 @@ class TestLegacyPartialTxFormat(TestCaseForTestnet):
wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config) wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)
tx = tx_from_any('cHNidP8BAJQCAAAAAcqqxrXrkW4wZ9AiT5QvszHOHc+0Axz7R555Qdz5XkCYAQAAAAD9////A6CGAQAAAAAAFgAU+fBLRlKk9v89xVEm2xJ0kG1wcvNMCwMAAAAAABepFPKffLiXEB3Gmv1Y35uy5bTUM59Nh0ANAwAAAAAAGXapFPriyJZefiOenIisUU3nDewLDxYIiKwSKxgATwEENYfPAAAAAAAAAAAAnOMnCVq57ruCJ7c38H6PtmrwS48+kcQJPEh70w/ofCQCDSEN062A0pw2JKkYltX2G3th8zLexPfEVDGu74BeD6cEcH3xxE8BBDWHzwGCB4l2gAAAAJOfYJjOAH6kksFOokIboP3+8Gwhlzlxhl5uY7zokvfcAmGy8e8txy0wkx69/TgZFOMe1aZc2g1HCwrRQ9M9+Ph7BLoE1bNPAQQ1h88BggeJdoAAAAFvoSi9wKWkb8evGv0gXbYgcZHUxAUbbtvQZBPNwLGi3wNqTky+e8Rm3WU3hMhrN3Sb7D6CgCnOo0wVaQlW/WFXbwQqQERnAAEA3wIAAAABnCh8O3p5TIeiImyJBHikJS+aVEdsCr+hgtTU3eimPIIBAAAAakcwRAIgTR7BvR+c9dHhx7CDnuJBXb52St/BOycfgpq7teEnUP4CIFp4DWI/xfKhwIZHZVPgYGOZLQC9jHiFKKiCSl7nXBUTASEDVPOeil/J5isfPp2yEcI6UQL8jFq6CRs/hegA8M2L/xj9////ApydBwAAAAAAGXapFLEFQG/gWw3IvkTHg2ulgS2/Z0zoiKwgoQcAAAAAABepFCwWF9JPk25A0UsN8pTst7uq11M3hxIrGAAiAgP+Qtq1hxjqBBP3yN5pPN7uIs4Zsdw0wLvdekgkVGXFokgwRQIhANA8NcspOwHae+DXcc7a+oke1dcKZ3z8zWlnE1b2vr7cAiALKldsTNHGQkgHVs4f1mCsrwulJhk6MJnh+BzdAa0gkwEBBGlSIQIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YiEDXy+CY7s2CNbMTuA71MuNZcTXCvcQSfBfv+5JeIMqH9IhA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiU64iBgP+Qtq1hxjqBBP3yN5pPN7uIs4Zsdw0wLvdekgkVGXFogy6BNWzAAAAAAAAAAAiBgIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YgwqQERnAAAAAAAAAAAiBgNfL4JjuzYI1sxO4DvUy41lxNcK9xBJ8F+/7kl4gyof0gxwffHEAAAAAAAAAAAAAAEAaVIhAiq2ef2i4zfECrvAggPT1x0qlv2784yQj3uS5TfBxO//IQNM0UcWdnXGYoZFDMVpQBP0N4OTY115ZKuKVzzD0EqNTiEDWOhaggFFh96oM+tf+5PjmFaQ7kumHvMtWyitWqFhc39TriICA0zRRxZ2dcZihkUMxWlAE/Q3g5NjXXlkq4pXPMPQSo1ODLoE1bMBAAAAAAAAACICAiq2ef2i4zfECrvAggPT1x0qlv2784yQj3uS5TfBxO//DCpARGcBAAAAAAAAACICA1joWoIBRYfeqDPrX/uT45hWkO5Lph7zLVsorVqhYXN/DHB98cQBAAAAAAAAAAAA') tx = tx_from_any('cHNidP8BAJQCAAAAAcqqxrXrkW4wZ9AiT5QvszHOHc+0Axz7R555Qdz5XkCYAQAAAAD9////A6CGAQAAAAAAFgAU+fBLRlKk9v89xVEm2xJ0kG1wcvNMCwMAAAAAABepFPKffLiXEB3Gmv1Y35uy5bTUM59Nh0ANAwAAAAAAGXapFPriyJZefiOenIisUU3nDewLDxYIiKwSKxgATwEENYfPAAAAAAAAAAAAnOMnCVq57ruCJ7c38H6PtmrwS48+kcQJPEh70w/ofCQCDSEN062A0pw2JKkYltX2G3th8zLexPfEVDGu74BeD6cEcH3xxE8BBDWHzwGCB4l2gAAAAJOfYJjOAH6kksFOokIboP3+8Gwhlzlxhl5uY7zokvfcAmGy8e8txy0wkx69/TgZFOMe1aZc2g1HCwrRQ9M9+Ph7CIIHiXYAAACATwEENYfPAYIHiXaAAAABb6EovcClpG/Hrxr9IF22IHGR1MQFG27b0GQTzcCxot8Dak5MvnvEZt1lN4TIazd0m+w+goApzqNMFWkJVv1hV28IggeJdgEAAIAAAQDfAgAAAAGcKHw7enlMh6IibIkEeKQlL5pUR2wKv6GC1NTd6KY8ggEAAABqRzBEAiBNHsG9H5z10eHHsIOe4kFdvnZK38E7Jx+Cmru14SdQ/gIgWngNYj/F8qHAhkdlU+BgY5ktAL2MeIUoqIJKXudcFRMBIQNU856KX8nmKx8+nbIRwjpRAvyMWroJGz+F6ADwzYv/GP3///8CnJ0HAAAAAAAZdqkUsQVAb+BbDci+RMeDa6WBLb9nTOiIrCChBwAAAAAAF6kULBYX0k+TbkDRSw3ylOy3u6rXUzeHEisYACICA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiSDBFAiEA0Dw1yyk7Adp74Ndxztr6iR7V1wpnfPzNaWcTVva+vtwCIAsqV2xM0cZCSAdWzh/WYKyvC6UmGTowmeH4HN0BrSCTAQEEaVIhAgkfC02KswAWpdHAiCSeAog/rYFg8G+lNYithZhlCj5iIQNfL4JjuzYI1sxO4DvUy41lxNcK9xBJ8F+/7kl4gyof0iED/kLatYcY6gQT98jeaTze7iLOGbHcNMC73XpIJFRlxaJTriIGA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiEIIHiXYAAACAAAAAAAAAAAAiBgIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YhCCB4l2AQAAgAAAAAAAAAAAIgYDXy+CY7s2CNbMTuA71MuNZcTXCvcQSfBfv+5JeIMqH9IMcH3xxAAAAAAAAAAAAAABAGlSIQIqtnn9ouM3xAq7wIID09cdKpb9u/OMkI97kuU3wcTv/yEDTNFHFnZ1xmKGRQzFaUAT9DeDk2NdeWSrilc8w9BKjU4hA1joWoIBRYfeqDPrX/uT45hWkO5Lph7zLVsorVqhYXN/U64iAgNM0UcWdnXGYoZFDMVpQBP0N4OTY115ZKuKVzzD0EqNThCCB4l2AAAAgAEAAAAAAAAAIgICKrZ5/aLjN8QKu8CCA9PXHSqW/bvzjJCPe5LlN8HE7/8QggeJdgEAAIABAAAAAAAAACICA1joWoIBRYfeqDPrX/uT45hWkO5Lph7zLVsorVqhYXN/DHB98cQBAAAAAAAAAAAA')
tx.add_info_from_wallet(wallet) tx.add_info_from_wallet(wallet)
raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet) raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet)
self.assertEqual('45505446ff000200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fd53010001ff01ff483045022100d03c35cb293b01da7be0d771cedafa891ed5d70a677cfccd69671356f6bebedc02200b2a576c4cd1c642480756ce1fd660acaf0ba526193a3099e1f81cdd01ad2093014d0201524c53ff043587cf0182078976800000016fa128bdc0a5a46fc7af1afd205db6207191d4c4051b6edbd06413cdc0b1a2df036a4e4cbe7bc466dd653784c86b37749bec3e828029cea34c15690956fd61576f000000004c53ff043587cf0000000000000000009ce327095ab9eebb8227b737f07e8fb66af04b8f3e91c4093c487bd30fe87c24020d210dd3ad80d29c3624a91896d5f61b7b61f332dec4f7c45431aeef805e0fa7000000004c53ff043587cf018207897680000000939f6098ce007ea492c14ea2421ba0fdfef06c21973971865e6e63bce892f7dc0261b2f1ef2dc72d30931ebdfd381914e31ed5a65cda0d470b0ad143d33df8f87b0000000053aefdffffff03a086010000000000160014f9f04b4652a4f6ff3dc55126db1274906d7072f34c0b03000000000017a914f29f7cb897101dc69afd58df9bb2e5b4d4339f4d87400d0300000000001976a914fae2c8965e7e239e9c88ac514de70dec0b0f160888ac122b1800', self.assertEqual('45505446ff000200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fd53010001ff01ff483045022100d03c35cb293b01da7be0d771cedafa891ed5d70a677cfccd69671356f6bebedc02200b2a576c4cd1c642480756ce1fd660acaf0ba526193a3099e1f81cdd01ad2093014d0201524c53ff043587cf0182078976800000016fa128bdc0a5a46fc7af1afd205db6207191d4c4051b6edbd06413cdc0b1a2df036a4e4cbe7bc466dd653784c86b37749bec3e828029cea34c15690956fd61576f000000004c53ff043587cf0000000000000000009ce327095ab9eebb8227b737f07e8fb66af04b8f3e91c4093c487bd30fe87c24020d210dd3ad80d29c3624a91896d5f61b7b61f332dec4f7c45431aeef805e0fa7000000004c53ff043587cf018207897680000000939f6098ce007ea492c14ea2421ba0fdfef06c21973971865e6e63bce892f7dc0261b2f1ef2dc72d30931ebdfd381914e31ed5a65cda0d470b0ad143d33df8f87b0000000053aefdffffff03a086010000000000160014f9f04b4652a4f6ff3dc55126db1274906d7072f34c0b03000000000017a914f29f7cb897101dc69afd58df9bb2e5b4d4339f4d87400d0300000000001976a914fae2c8965e7e239e9c88ac514de70dec0b0f160888ac122b1800',
@ -795,7 +795,7 @@ class TestLegacyPartialTxFormat(TestCaseForTestnet):
wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config) wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)
tx = tx_from_any('cHNidP8BAJ8CAAAAAYfEZGymkLOX41eyOyE3AwaRqQoGimaQg000C0voSs1qAQAAAAD9////A6CGAQAAAAAAFgAUi8nZR+TQrdwvTDS4NxA060ez0wUUDAMAAAAAACIAIKYIfE+EpV3DkBRymhjgiVUTnUOEVZ0f0qSKHZXXRqQlQA0DAAAAAAAZdqkUwT/WKU0b57lBClU49LTvEPxZTueIrBMrGABPAQJXVIMAAAAAAAAAAADWRNzdekrLQyNV4BCsSl+VWUDIKpdncxt9idxC6zzaxAJy+qL5i3bMnWVe8oHAes2nXDCpkNw6Unts+SqPWmuKgARL8hIJTwECV1SDAdJM1ZGAAAABmcTWyJP6Gt3sawEhGBE34lw4GUMzuMVyFbPPHm1+evECoj3a5pi7YJW4uANb3R6UR59mwpZ52Bkx4P4HqkNhSe4E5U2yWk8BAldUgwHSTNWRgAAAALYKhQia0IUKb/6FkKPhUGTmPu/QIIE98uSmsyCc499fAsixTykX1VfNSClwwRTD40V+CdJWOXgCJ3ouTRUZq5+MBAHZMVwAAQEqIKEHAAAAAAAAIKlI1/pqu7l+MXea5UODAStBPVOCHH/TlJAPa0Q8Yd7uIgIDB6PEHQftl21l4hPoI9AoQJN0decJtBJT6F6XDjyxZnRHMEQCIC975frTmPGjV2KTM58idNInrxd5hpCqGtAP+D2WclzFAiAAyy6f/1viOoYnGMZlpQNfFyeD6bMVjq6DZz9sWa3DwAEBBWlSIQKp3LVw6CgMdB8JAywVgJW3qjsM5AGtoDDy1HuZnwIGBiEDB6PEHQftl21l4hPoI9AoQJN0decJtBJT6F6XDjyxZnQhA1IbCkXgQvCMzQOvR/2IuyB7VBTg4wu4eZ/KMRoGMjoZU64iBgMHo8QdB+2XbWXiE+gj0ChAk3R15wm0ElPoXpcOPLFmdAwB2TFcAAAAAAEAAAAiBgNSGwpF4ELwjM0Dr0f9iLsge1QU4OMLuHmfyjEaBjI6GQzlTbJaAAAAAAEAAAAiBgKp3LVw6CgMdB8JAywVgJW3qjsM5AGtoDDy1HuZnwIGBgxL8hIJAAAAAAEAAAAAAAEBaVIhAgWCn5UiV3EiypypvrZ/lM8v4K0NF+BSoRBwHPQSjTOcIQIsyigs/dnMHzh9MJhmHWi8nIo5rGvXLDDbV5p4V9CFmyEC2Q48iXOES5zAs4SUxIUVoxnuw6wkiflvb1XmqmkSpghTriICAtkOPIlzhEucwLOElMSFFaMZ7sOsJIn5b29V5qppEqYIDAHZMVwBAAAAAAAAACICAizKKCz92cwfOH0wmGYdaLycijmsa9csMNtXmnhX0IWbDOVNsloBAAAAAAAAACICAgWCn5UiV3EiypypvrZ/lM8v4K0NF+BSoRBwHPQSjTOcDEvyEgkBAAAAAAAAAAAA') tx = tx_from_any('cHNidP8BAJ8CAAAAAYfEZGymkLOX41eyOyE3AwaRqQoGimaQg000C0voSs1qAQAAAAD9////A6CGAQAAAAAAFgAUi8nZR+TQrdwvTDS4NxA060ez0wUUDAMAAAAAACIAIKYIfE+EpV3DkBRymhjgiVUTnUOEVZ0f0qSKHZXXRqQlQA0DAAAAAAAZdqkUwT/WKU0b57lBClU49LTvEPxZTueIrBMrGABPAQJXVIMAAAAAAAAAAADWRNzdekrLQyNV4BCsSl+VWUDIKpdncxt9idxC6zzaxAJy+qL5i3bMnWVe8oHAes2nXDCpkNw6Unts+SqPWmuKgARL8hIJTwECV1SDAdJM1ZGAAAABmcTWyJP6Gt3sawEhGBE34lw4GUMzuMVyFbPPHm1+evECoj3a5pi7YJW4uANb3R6UR59mwpZ52Bkx4P4HqkNhSe4I0kzVkQEAAIBPAQJXVIMB0kzVkYAAAAC2CoUImtCFCm/+hZCj4VBk5j7v0CCBPfLkprMgnOPfXwLIsU8pF9VXzUgpcMEUw+NFfgnSVjl4Aid6Lk0VGaufjAjSTNWRAAAAgAABASogoQcAAAAAAAAgqUjX+mq7uX4xd5rlQ4MBK0E9U4Icf9OUkA9rRDxh3u4iAgMHo8QdB+2XbWXiE+gj0ChAk3R15wm0ElPoXpcOPLFmdEcwRAIgL3vl+tOY8aNXYpMznyJ00ievF3mGkKoa0A/4PZZyXMUCIADLLp//W+I6hicYxmWlA18XJ4PpsxWOroNnP2xZrcPAAQEFaVIhAqnctXDoKAx0HwkDLBWAlbeqOwzkAa2gMPLUe5mfAgYGIQMHo8QdB+2XbWXiE+gj0ChAk3R15wm0ElPoXpcOPLFmdCEDUhsKReBC8IzNA69H/Yi7IHtUFODjC7h5n8oxGgYyOhlTriIGAwejxB0H7ZdtZeIT6CPQKECTdHXnCbQSU+helw48sWZ0ENJM1ZEAAACAAAAAAAEAAAAiBgNSGwpF4ELwjM0Dr0f9iLsge1QU4OMLuHmfyjEaBjI6GRDSTNWRAQAAgAAAAAABAAAAIgYCqdy1cOgoDHQfCQMsFYCVt6o7DOQBraAw8tR7mZ8CBgYMS/ISCQAAAAABAAAAAAABAWlSIQIFgp+VIldxIsqcqb62f5TPL+CtDRfgUqEQcBz0Eo0znCECLMooLP3ZzB84fTCYZh1ovJyKOaxr1yww21eaeFfQhZshAtkOPIlzhEucwLOElMSFFaMZ7sOsJIn5b29V5qppEqYIU64iAgLZDjyJc4RLnMCzhJTEhRWjGe7DrCSJ+W9vVeaqaRKmCBDSTNWRAAAAgAEAAAAAAAAAIgICLMooLP3ZzB84fTCYZh1ovJyKOaxr1yww21eaeFfQhZsQ0kzVkQEAAIABAAAAAAAAACICAgWCn5UiV3EiypypvrZ/lM8v4K0NF+BSoRBwHPQSjTOcDEvyEgkBAAAAAAAAAAAA')
tx.add_info_from_wallet(wallet) tx.add_info_from_wallet(wallet)
raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet) raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet)
self.assertEqual('45505446ff000200000000010187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788acfeffffffff20a10700000000000000050001ff47304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c00101fffd0201524c53ff02575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80000001004c53ff0257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c000001004c53ff0257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee0000010053ae132b1800', self.assertEqual('45505446ff000200000000010187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788acfeffffffff20a10700000000000000050001ff47304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c00101fffd0201524c53ff02575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80000001004c53ff0257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c000001004c53ff0257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee0000010053ae132b1800',

View file

@ -1453,7 +1453,8 @@ class PartialTransaction(Transaction):
raise SerializationError(f"duplicate key: {repr(kt)}") raise SerializationError(f"duplicate key: {repr(kt)}")
xfp, path = unpack_bip32_root_fingerprint_and_int_path(val) xfp, path = unpack_bip32_root_fingerprint_and_int_path(val)
if bip32node.depth != len(path): if bip32node.depth != len(path):
raise SerializationError(f"PSBT global xpub has mismatching depth and derivation prefix len") raise SerializationError(f"PSBT global xpub has mismatching depth ({bip32node.depth}) "
f"and derivation prefix len ({len(path)})")
child_number_of_xpub = int.from_bytes(bip32node.child_number, 'big') child_number_of_xpub = int.from_bytes(bip32node.child_number, 'big')
if not ((bip32node.depth == 0 and child_number_of_xpub == 0) if not ((bip32node.depth == 0 and child_number_of_xpub == 0)
or (bip32node.depth != 0 and child_number_of_xpub == path[-1])): or (bip32node.depth != 0 and child_number_of_xpub == path[-1])):
@ -1736,10 +1737,9 @@ class PartialTransaction(Transaction):
from .keystore import Xpub from .keystore import Xpub
for ks in wallet.get_keystores(): for ks in wallet.get_keystores():
if isinstance(ks, Xpub): if isinstance(ks, Xpub):
bip32node = BIP32Node.from_xkey(ks.get_master_public_key()) fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[])
xfp_bytes = bfh(ks.get_root_fingerprint()) bip32node = BIP32Node.from_xkey(ks.get_xpub_to_be_used_in_partial_tx())
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix()) self.xpubs[bip32node] = (fp_bytes, der_full)
self.xpubs[bip32node] = (xfp_bytes, der_prefix)
for txin in self.inputs(): for txin in self.inputs():
wallet.add_input_info(txin) wallet.add_input_info(txin)
for txout in self.outputs(): for txout in self.outputs():

View file

@ -285,6 +285,8 @@ class Abstract_Wallet(AddressSynchronizer):
def stop_threads(self): def stop_threads(self):
super().stop_threads() super().stop_threads()
if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]):
self.save_keystore()
self.storage.write() self.storage.write()
def set_up_to_date(self, b): def set_up_to_date(self, b):
@ -318,6 +320,9 @@ class Abstract_Wallet(AddressSynchronizer):
def get_master_public_key(self): def get_master_public_key(self):
return None return None
def get_master_public_keys(self):
return []
def basename(self) -> str: def basename(self) -> str:
return os.path.basename(self.storage.path) return os.path.basename(self.storage.path)
@ -1224,6 +1229,9 @@ class Abstract_Wallet(AddressSynchronizer):
def _add_input_sig_info(self, txin: PartialTxInput, address: str) -> None: def _add_input_sig_info(self, txin: PartialTxInput, address: str) -> None:
raise NotImplementedError() # implemented by subclasses raise NotImplementedError() # implemented by subclasses
def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput], address: str) -> None:
pass # implemented by subclasses
def _add_input_utxo_info(self, txin: PartialTxInput, address: str) -> None: def _add_input_utxo_info(self, txin: PartialTxInput, address: str) -> None:
if Transaction.is_segwit_input(txin): if Transaction.is_segwit_input(txin):
if txin.witness_utxo is None: if txin.witness_utxo is None:
@ -1312,16 +1320,7 @@ class Abstract_Wallet(AddressSynchronizer):
txout.is_change = self.is_change(address) txout.is_change = self.is_change(address)
if isinstance(self, Multisig_Wallet): if isinstance(self, Multisig_Wallet):
txout.num_sig = self.m txout.num_sig = self.m
if isinstance(self, Deterministic_Wallet): self._add_txinout_derivation_info(txout, address)
if not txout.pubkeys or len(txout.pubkeys) != len(txout.bip32_paths):
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
txout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)])
for pubkey_hex in pubkey_deriv_info:
ks, der_suffix = pubkey_deriv_info[pubkey_hex]
xfp_bytes = bfh(ks.get_root_fingerprint())
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
der_full = der_prefix + list(der_suffix)
txout.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
if txout.redeem_script is None: if txout.redeem_script is None:
try: try:
redeem_script_hex = self.get_redeem_script(address) redeem_script_hex = self.get_redeem_script(address)
@ -1721,6 +1720,9 @@ class Abstract_Wallet(AddressSynchronizer):
def get_keystores(self) -> Sequence[KeyStore]: def get_keystores(self) -> Sequence[KeyStore]:
return [self.keystore] if self.keystore else [] return [self.keystore] if self.keystore else []
def save_keystore(self):
raise NotImplementedError()
class Simple_Wallet(Abstract_Wallet): class Simple_Wallet(Abstract_Wallet):
# wallet with a single keystore # wallet with a single keystore
@ -1773,9 +1775,6 @@ class Imported_Wallet(Simple_Wallet):
def is_change(self, address): def is_change(self, address):
return False return False
def get_master_public_keys(self):
return []
def is_beyond_limit(self, address): def is_beyond_limit(self, address):
return False return False
@ -1903,7 +1902,8 @@ class Imported_Wallet(Simple_Wallet):
return self.db.get_imported_address(address).get('type', 'address') return self.db.get_imported_address(address).get('type', 'address')
def _add_input_sig_info(self, txin, address): def _add_input_sig_info(self, txin, address):
assert self.is_mine(address) if not self.is_mine(address):
return
if txin.script_type in ('unknown', 'address'): if txin.script_type in ('unknown', 'address'):
return return
elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
@ -2014,15 +2014,17 @@ class Deterministic_Wallet(Abstract_Wallet):
for k in self.get_keystores()} for k in self.get_keystores()}
def _add_input_sig_info(self, txin, address): def _add_input_sig_info(self, txin, address):
assert self.is_mine(address) self._add_txinout_derivation_info(txin, address)
def _add_txinout_derivation_info(self, txinout, address):
if not self.is_mine(address):
return
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address) pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
txin.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)]) txinout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)])
for pubkey_hex in pubkey_deriv_info: for pubkey_hex in pubkey_deriv_info:
ks, der_suffix = pubkey_deriv_info[pubkey_hex] ks, der_suffix = pubkey_deriv_info[pubkey_hex]
xfp_bytes = bfh(ks.get_root_fingerprint()) fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix)
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix()) txinout.bip32_paths[bfh(pubkey_hex)] = (fp_bytes, der_full)
der_full = der_prefix + list(der_suffix)
txin.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
def create_new_address(self, for_change=False): def create_new_address(self, for_change=False):
assert type(for_change) is bool assert type(for_change) is bool