Browse Source

Add watch only wallets for fidelity bonds

Watch only wallets can now be created via wallet-tool. The wallets
store a bip32 xpub key from which all the public keys are generated.

Watch only wallets only store and display the zeroth mixdepth, which
is the only one needed for fidelity bonds.

The bip32 xpub key needed to create a watch only wallet is now
specially highlighted in the wallet-tool display, this is to help users
actually find it amongst all the other xpubs.

The field key_ident in the wallet class was previously generated using
private keys, which are not available in watch only wallets. So now
for fidelity bond wallets key_ident is generated using a public key.
Existing non-fidelity-bond wallets are unaffected
master
chris-belcher 6 years ago
parent
commit
762b1f644d
No known key found for this signature in database
GPG Key ID: EF734EA677F31129
  1. 2
      jmbitcoin/jmbitcoin/secp256k1_deterministic.py
  2. 4
      jmclient/jmclient/__init__.py
  3. 64
      jmclient/jmclient/cryptoengine.py
  4. 56
      jmclient/jmclient/wallet.py
  5. 43
      jmclient/jmclient/wallet_utils.py

2
jmbitcoin/jmbitcoin/secp256k1_deterministic.py

@ -65,6 +65,8 @@ def bip32_deserialize(data):
def raw_bip32_privtopub(rawtuple):
vbytes, depth, fingerprint, i, chaincode, key = rawtuple
if vbytes in PUBLIC:
return rawtuple
newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC
return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key, False))

4
jmclient/jmclient/__init__.py

@ -13,8 +13,8 @@ from .old_mnemonic import mn_decode, mn_encode
from .taker import Taker, P2EPTaker
from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportWalletMixin,
BIP39WalletMixin, BIP32Wallet, BIP49Wallet, LegacyWallet,
SegwitWallet, SegwitLegacyWallet, FidelityBondMixin, UTXOManager,
WALLET_IMPLEMENTATIONS, compute_tx_locktime)
SegwitWallet, SegwitLegacyWallet, FidelityBondMixin, FidelityBondWatchonlyWallet,
UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime)
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage)
from .cryptoengine import BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, EngineError

64
jmclient/jmclient/cryptoengine.py

@ -7,9 +7,13 @@ import struct
import jmbitcoin as btc
from .configure import get_network, jm_single
#NOTE: before fidelity bonds and watchonly wallet, each of these types corresponded
# to one wallet type and one engine, not anymore
#with fidelity bond wallets and watchonly fidelity bond wallet, the wallet class
# can have two engines, one for single-sig addresses and the other for timelocked addresses
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS = range(6)
TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS, TYPE_WATCHONLY_FIDELITY_BONDS, \
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH = range(9)
NET_MAINNET, NET_TESTNET = range(2)
NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET}
WIF_PREFIX_MAP = {'mainnet': b'\x80', 'testnet': b'\xef'}
@ -130,6 +134,7 @@ class BTCEngine(object):
@classmethod
def derive_bip32_pub_export(cls, master_key, path):
#in the case of watchonly wallets this priv is actually a pubkey
priv = cls._walk_bip32_path(master_key, path)
return btc.bip32_serialize(btc.raw_bip32_privtopub(priv))
@ -336,9 +341,62 @@ class BTC_Timelocked_P2WSH(BTCEngine):
hashcode=btc.SIGHASH_ALL, **kwargs):
raise RuntimeError("Cannot spend from watch-only wallets")
class BTC_Watchonly_Timelocked_P2WSH(BTC_Timelocked_P2WSH):
@classmethod
def get_watchonly_path(cls, path):
#given path is something like "m/49'/1'/0'/0/0"
#but watchonly wallet already stores the xpub for "m/49'/1'/0'/"
#so to make this work we must chop off the first 3 elements
return path[3:]
@classmethod
def derive_bip32_privkey(cls, master_key, path):
assert len(path) > 1
return cls._walk_bip32_path(master_key, cls.get_watchonly_path(
path))[-1]
@classmethod
def privkey_to_script(cls, pubkey_locktime):
pub, locktime = pubkey_locktime
return cls.pubkey_to_script((pub, locktime))
@classmethod
def privkey_to_wif(cls, privkey_locktime):
return ""
@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
raise Exception("not implemented yet")
class BTC_Watchonly_P2SH_P2WPKH(BTC_P2SH_P2WPKH):
@classmethod
def derive_bip32_privkey(cls, master_key, path):
return BTC_Watchonly_Timelocked_P2WSH.derive_bip32_privkey(master_key, path)
@classmethod
def privkey_to_wif(cls, privkey_locktime):
return BTC_Watchonly_Timelocked_P2WSH.privkey_to_wif(privkey_locktime)
@staticmethod
def privkey_to_pubkey(privkey):
#in watchonly wallets there are no privkeys, so functions
# like _get_priv_from_path() actually return pubkeys and
# this function is a noop
return privkey
@classmethod
def derive_bip32_pub_export(cls, master_key, path):
return super(BTC_Watchonly_P2SH_P2WPKH, cls).derive_bip32_pub_export(
master_key, BTC_Watchonly_Timelocked_P2WSH.get_watchonly_path(path))
ENGINES = {
TYPE_P2PKH: BTC_P2PKH,
TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH,
TYPE_P2WPKH: BTC_P2WPKH,
TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH
TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH,
TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH,
TYPE_WATCHONLY_P2SH_P2WPKH: BTC_Watchonly_P2SH_P2WPKH
}

56
jmclient/jmclient/wallet.py

@ -22,6 +22,7 @@ from .support import select_gradual, select_greedy, select_greediest, \
select
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH,\
TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS,\
TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH,\
ENGINES
from .support import get_random_bytes
from . import mn_encode, mn_decode
@ -1303,9 +1304,7 @@ class BIP32Wallet(BaseWallet):
# used to verify paths for sanity checking and for wallet id creation
self._key_ident = b'' # otherwise get_bip32_* won't work
self._key_ident = sha256(sha256(
self.get_bip32_priv_export(0, 0).encode('ascii')).digest())\
.digest()[:3]
self._key_ident = self._get_key_ident()
self._populate_script_map()
self.disable_new_scripts = False
@ -1347,6 +1346,11 @@ class BIP32Wallet(BaseWallet):
self.max_mixdepth = max(0, 0, *self._index_cache.keys())
def _get_key_ident(self):
return sha256(sha256(
self.get_bip32_priv_export(0, 0).encode('ascii')).digest())\
.digest()[:3]
def _populate_script_map(self):
for md in self._index_cache:
for address_type in (self.BIP32_EXT_ID, self.BIP32_INT_ID):
@ -1652,6 +1656,8 @@ class FidelityBondMixin(object):
"""
TIMELOCK_GAP_LIMIT_REDUCTION_FACTOR = 6
_TIMELOCK_ENGINE = ENGINES[TYPE_TIMELOCK_P2WSH]
#only one mixdepth will have fidelity bonds in it
FIDELITY_BOND_MIXDEPTH = 0
@ -1659,6 +1665,8 @@ class FidelityBondMixin(object):
_BURNER_OUTPUT_STORAGE_KEY = b"burner-out"
_BIP32_PUBKEY_PREFIX = "fbonds-mpk-"
@classmethod
def _time_number_to_timestamp(cls, timenumber):
"""
@ -1701,6 +1709,13 @@ class FidelityBondMixin(object):
pub = engine.privkey_to_pubkey(priv)
return sha256(sha256(pub).digest()).digest()[:3]
@classmethod
def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk):
if mpk.startswith(cls._BIP32_PUBKEY_PREFIX):
return mpk[len(cls._BIP32_PUBKEY_PREFIX):]
else:
return False
def _populate_script_map(self):
super(FidelityBondMixin, self)._populate_script_map()
for md in self._index_cache:
@ -1711,6 +1726,12 @@ class FidelityBondMixin(object):
script = self.get_script_from_path(path)
self._script_map[script] = path
def get_bip32_pub_export(self, mixdepth=None, address_type=None):
bip32_pub = super(FidelityBondMixin, self).get_bip32_pub_export(mixdepth, address_type)
if address_type == None and mixdepth == self.FIDELITY_BOND_MIXDEPTH:
bip32_pub = self._BIP32_PUBKEY_PREFIX + bip32_pub
return bip32_pub
@classmethod
def _get_supported_address_types(cls):
return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID, cls.BIP32_TIMELOCK_ID, cls.BIP32_BURN_ID)
@ -1719,7 +1740,7 @@ class FidelityBondMixin(object):
if self._is_timelocked_path(path):
key_path = path[:-1]
locktime = path[-1]
engine = ENGINES[TYPE_TIMELOCK_P2WSH]
engine = self._TIMELOCK_ENGINE
privkey = engine.derive_bip32_privkey(self._master_key, key_path)
return (privkey, locktime), engine
else:
@ -1807,10 +1828,6 @@ class FidelityBondMixin(object):
self._storage.data[self._BURNER_OUTPUT_STORAGE_KEY][path][2] = \
merkle_branch
#class FidelityBondWatchonlyWallet(ImportWalletMixin, BIP39WalletMixin, FidelityBondMixin):
class BIP49Wallet(BIP32PurposedWallet):
_PURPOSE = 2**31 + 49
_ENGINE = ENGINES[TYPE_P2SH_P2WPKH]
@ -1828,9 +1845,30 @@ class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, BIP84Wallet):
class SegwitLegacyWalletFidelityBonds(FidelityBondMixin, SegwitLegacyWallet):
TYPE = TYPE_SEGWIT_LEGACY_WALLET_FIDELITY_BONDS
class FidelityBondWatchonlyWallet(FidelityBondMixin, BIP49Wallet):
TYPE = TYPE_WATCHONLY_FIDELITY_BONDS
_ENGINE = ENGINES[TYPE_WATCHONLY_P2SH_P2WPKH]
_TIMELOCK_ENGINE = ENGINES[TYPE_WATCHONLY_TIMELOCK_P2WSH]
@classmethod
def _verify_entropy(cls, ent):
return ent[1:4] == b"pub"
@classmethod
def _derive_bip32_master_key(cls, master_entropy):
return btc.bip32_deserialize(master_entropy.decode())
def _get_bip32_export_path(self, mixdepth=None, address_type=None):
path = super(FidelityBondWatchonlyWallet, self)._get_bip32_export_path(
mixdepth, address_type)
return path
WALLET_IMPLEMENTATIONS = {
LegacyWallet.TYPE: LegacyWallet,
SegwitLegacyWallet.TYPE: SegwitLegacyWallet,
SegwitWallet.TYPE: SegwitWallet,
SegwitLegacyWalletFidelityBonds.TYPE: SegwitLegacyWalletFidelityBonds
SegwitLegacyWalletFidelityBonds.TYPE: SegwitLegacyWalletFidelityBonds,
FidelityBondWatchonlyWallet.TYPE: FidelityBondWatchonlyWallet
}

43
jmclient/jmclient/wallet_utils.py

@ -13,8 +13,8 @@ from itertools import islice
from jmclient import (get_network, WALLET_IMPLEMENTATIONS, Storage, podle,
jm_single, BitcoinCoreInterface, WalletError,
VolatileStorage, StoragePasswordError, is_segwit_mode, SegwitLegacyWallet,
LegacyWallet, SegwitWallet, FidelityBondMixin, is_native_segwit_mode,
load_program_config, add_base_options, check_regtest)
LegacyWallet, SegwitWallet, FidelityBondMixin, FidelityBondWatchonlyWallet,
is_native_segwit_mode, load_program_config, add_base_options, check_regtest)
from jmclient.wallet_service import WalletService
from jmbase.support import get_password, jmprint, EXIT_FAILURE, EXIT_ARGERROR
@ -47,7 +47,8 @@ def get_wallettool_parser():
'(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.\n'
'(gettimelockaddress) Obtain a timelocked address. Argument is locktime value as yyyy-mm. For example `2021-03`\n'
'(addtxoutproof) Add a tx out proof as metadata to a burner transaction. Specify path with '
'-H and proof which is output of Bitcoin Core\'s RPC call gettxoutproof')
'-H and proof which is output of Bitcoin Core\'s RPC call gettxoutproof\n'
'(createwatchonly) Create a watch-only fidelity bond wallet')
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method] [args..]',
description=description)
add_base_options(parser)
@ -485,7 +486,6 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
entrylist.append(WalletViewEntry(
wallet_service.get_path_repr(path), m, address_type, k,
addr, [balance, balance], priv=privkey, used=status))
#TODO fidelity bond master pub key is this, although it should include burner too
xpub_key = wallet_service.get_bip32_pub_export(m, address_type)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
@ -556,8 +556,8 @@ def cli_get_wallet_passphrase_check():
return False
return password
def cli_get_wallet_file_name():
return input('Input wallet file name (default: wallet.jmdat): ')
def cli_get_wallet_file_name(defaultname="wallet.jmdat"):
return input('Input wallet file name (default: ' + defaultname + '): ')
def cli_display_user_words(words, mnemonic_extension):
text = 'Write down this wallet recovery mnemonic\n\n' + words +'\n'
@ -1176,6 +1176,30 @@ def wallet_addtxoutproof(wallet_service, hdpath, txoutproof):
new_merkle_branch, block_index)
return "Done"
def wallet_createwatchonly(wallet_root_path, master_pub_key):
wallet_name = cli_get_wallet_file_name(defaultname="watchonly.jmdat")
if not wallet_name:
DEFAULT_WATCHONLY_WALLET_NAME = "watchonly.jmdat"
wallet_name = DEFAULT_WATCHONLY_WALLET_NAME
wallet_path = os.path.join(wallet_root_path, wallet_name)
password = cli_get_wallet_passphrase_check()
if not password:
return ""
entropy = FidelityBondMixin.get_xpub_from_fidelity_bond_master_pub_key(master_pub_key)
if not entropy:
jmprint("Error with provided master pub key", "error")
return ""
entropy = entropy.encode()
wallet = create_wallet(wallet_path, password,
max_mixdepth=FidelityBondMixin.FIDELITY_BOND_MIXDEPTH,
wallet_cls=FidelityBondWatchonlyWallet, entropy=entropy)
return "Done"
def get_configured_wallet_type(support_fidelity_bonds):
configured_type = TYPE_P2PKH
if is_segwit_mode():
@ -1337,7 +1361,7 @@ def wallet_tool_main(wallet_root_path):
check_regtest(blockchain_start=False)
# full path to the wallets/ subdirectory in the user data area:
wallet_root_path = os.path.join(jm_single().datadir, wallet_root_path)
noseed_methods = ['generate', 'recover']
noseed_methods = ['generate', 'recover', 'createwatchonly']
methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey',
'history', 'showutxos', 'freeze', 'gettimelockaddress', 'addtxoutproof']
methods.extend(noseed_methods)
@ -1440,6 +1464,11 @@ def wallet_tool_main(wallet_root_path):
+ 'Core\'s RPC call gettxoutproof', "error")
sys.exit(EXIT_ARGERROR)
return wallet_addtxoutproof(wallet_service, options.hd_path, args[2])
elif method == "createwatchonly":
if len(args) < 2:
jmprint("args: [master public key]", "error")
sys.exit(EXIT_ARGERROR)
return wallet_createwatchonly(wallet_root_path, args[1])
else:
parser.error("Unknown wallet-tool method: " + method)
sys.exit(EXIT_ARGERROR)

Loading…
Cancel
Save