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

Implement general purpose watch only wallet, take 2 #1632

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/fidelity-bonds.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ is highlighted with a prefix `fbonds-mpk-`.
This master public key can be used to create a watch-only wallet using
`wallet-tool.py`.

$ python3 wallet-tool.py createwatchonly fbonds-mpk-tpubDDCbCPdf5wJVGYWB4mZr3E3Lys4NBcEKysrrUrLfhG6sekmrvs6KZNe4i5p5z3FyfwRmKMqB9NWEcEUiTS4LwqfrKPQzhKj6aLihu2EejaU
$ python3 wallet-tool.py createfbwatchonly fbonds-mpk-tpubDDCbCPdf5wJVGYWB4mZr3E3Lys4NBcEKysrrUrLfhG6sekmrvs6KZNe4i5p5z3FyfwRmKMqB9NWEcEUiTS4LwqfrKPQzhKj6aLihu2EejaU
Input wallet file name (default: watchonly.jmdat): watchfidelity.jmdat
Enter wallet file encryption passphrase:
Reenter wallet file encryption passphrase:
Expand Down
5 changes: 4 additions & 1 deletion src/jmbitcoin/secp256k1_deterministic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
# Below code ASSUMES binary inputs and compressed pubkeys
MAINNET_PRIVATE = b'\x04\x88\xAD\xE4'
MAINNET_PUBLIC = b'\x04\x88\xB2\x1E'
MAINNET_PUBLIC_P2SH_P2WPKH = b'\x04\x9D\x7C\xB2'
MAINNET_PUBLIC_P2WPKH = b'\x04\xB2\x47\x46'

TESTNET_PRIVATE = b'\x04\x35\x83\x94'
TESTNET_PUBLIC = b'\x04\x35\x87\xCF'
SIGNET_PRIVATE = b'\x04\x35\x83\x94'
SIGNET_PUBLIC = b'\x04\x35\x87\xCF'
PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE, SIGNET_PRIVATE]
PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC, SIGNET_PUBLIC]
PUBLIC = [MAINNET_PUBLIC, MAINNET_PUBLIC_P2SH_P2WPKH, MAINNET_PUBLIC_P2WPKH, TESTNET_PUBLIC, SIGNET_PUBLIC]

privtopub = privkey_to_pubkey

Expand Down
32 changes: 31 additions & 1 deletion src/jmclient/cryptoengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
# make existing wallets unsable.
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \
TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_WATCHONLY_FIDELITY_BONDS, \
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, TYPE_P2TR = range(11)
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, \
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, TYPE_P2TR = range(15)
NET_MAINNET, NET_TESTNET, NET_SIGNET = range(3)
NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET,
'signet': NET_SIGNET}
Expand Down Expand Up @@ -431,6 +432,34 @@ def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
raise RuntimeError("Cannot spend from watch-only wallets")

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_key_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))

@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
raise RuntimeError("Cannot spend from watch-only wallets")


class BTC_Watchonly_P2WPKH(BTC_P2WPKH):

@classmethod
Expand Down Expand Up @@ -464,6 +493,7 @@ def sign_transaction(cls, tx, index, privkey, amount,
TYPE_P2WPKH: BTC_P2WPKH,
TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH,
TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH,
TYPE_WATCHONLY_P2SH_P2WPKH: BTC_Watchonly_P2SH_P2WPKH,
TYPE_WATCHONLY_P2WPKH: BTC_Watchonly_P2WPKH,
TYPE_SEGWIT_WALLET_FIDELITY_BONDS: BTC_P2WPKH,
TYPE_P2TR: None # TODO
Expand Down
43 changes: 35 additions & 8 deletions src/jmclient/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WSH,\
TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS,\
TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, \
TYPE_WATCHONLY_P2WPKH, TYPE_P2TR, ENGINES, detect_script_type, EngineError
TYPE_WATCHONLY_P2SH_P2WPKH, TYPE_WATCHONLY_P2WPKH, TYPE_P2TR, ENGINES, \
detect_script_type, EngineError
from .support import get_random_bytes
from . import mn_encode, mn_decode
import jmbitcoin as btc
Expand Down Expand Up @@ -2808,14 +2809,10 @@ class SegwitLegacyWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, S
class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, SNICKERWalletMixin, BIP84Wallet):
TYPE = TYPE_P2WPKH

class SegwitWalletFidelityBonds(FidelityBondMixin, SegwitWallet):
TYPE = TYPE_SEGWIT_WALLET_FIDELITY_BONDS


class FidelityBondWatchonlyWallet(FidelityBondMixin, BIP84Wallet):
TYPE = TYPE_WATCHONLY_FIDELITY_BONDS
_ENGINE = ENGINES[TYPE_WATCHONLY_P2WPKH]
_TIMELOCK_ENGINE = ENGINES[TYPE_WATCHONLY_TIMELOCK_P2WSH]
class WatchonlyMixin(object):
# When watching an external wallet, we only watch account 0
WATCH_ONLY_MIXDEPTH = 0

@classmethod
def _verify_entropy(cls, ent):
Expand All @@ -2825,6 +2822,34 @@ def _verify_entropy(cls, ent):
def _derive_bip32_master_key(cls, master_entropy):
return btc.bip32_deserialize(master_entropy.decode())

class SegwitLegacyWatchonlyWallet(WatchonlyMixin, BIP49Wallet):
TYPE = TYPE_WATCHONLY_P2SH_P2WPKH
_ENGINE = ENGINES[TYPE_WATCHONLY_P2SH_P2WPKH]

def _get_key_ident(self):
return sha256(sha256(
self.get_bip32_pub_export(0, self.BIP32_EXT_ID).encode('ascii')).digest())\
.digest()[:3]

class SegwitWatchonlyWallet(WatchonlyMixin, BIP84Wallet):
TYPE = TYPE_WATCHONLY_P2WPKH
_ENGINE = ENGINES[TYPE_WATCHONLY_P2WPKH]

def _get_key_ident(self):
return sha256(sha256(
self.get_bip32_pub_export(0, self.BIP32_EXT_ID).encode('ascii')).digest())\
.digest()[:3]


class SegwitWalletFidelityBonds(FidelityBondMixin, SegwitWallet):
TYPE = TYPE_SEGWIT_WALLET_FIDELITY_BONDS


class FidelityBondWatchonlyWallet(FidelityBondMixin, WatchonlyMixin, BIP84Wallet):
TYPE = TYPE_WATCHONLY_FIDELITY_BONDS
_ENGINE = ENGINES[TYPE_WATCHONLY_P2WPKH]
_TIMELOCK_ENGINE = ENGINES[TYPE_WATCHONLY_TIMELOCK_P2WSH]

def _get_bip32_export_path(self, mixdepth=None, address_type=None):
path = super()._get_bip32_export_path(mixdepth, address_type)
return path
Expand Down Expand Up @@ -2871,6 +2896,8 @@ def _get_pubkey_from_path(self, path,
LegacyWallet.TYPE: LegacyWallet,
SegwitLegacyWallet.TYPE: SegwitLegacyWallet,
SegwitWallet.TYPE: SegwitWallet,
SegwitLegacyWatchonlyWallet.TYPE: SegwitLegacyWatchonlyWallet,
SegwitWatchonlyWallet.TYPE: SegwitWatchonlyWallet,
SegwitWalletFidelityBonds.TYPE: SegwitWalletFidelityBonds,
FidelityBondWatchonlyWallet.TYPE: FidelityBondWatchonlyWallet
}
51 changes: 40 additions & 11 deletions src/jmclient/wallet_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
is_native_segwit_mode, load_program_config, add_base_options, check_regtest)
from jmclient.blockchaininterface import (BitcoinCoreInterface,
BitcoinCoreNoHistoryInterface)
from jmclient.wallet import SegwitLegacyWatchonlyWallet, SegwitWatchonlyWallet, WatchonlyMixin
from jmclient.wallet_service import WalletService
from jmbase.support import (get_password, jmprint, EXIT_FAILURE,
EXIT_ARGERROR, utxo_to_utxostr, hextobin, bintohex,
Expand Down Expand Up @@ -54,7 +55,8 @@ def get_wallettool_parser():
(gettimelockaddress) Obtain a timelocked address. Argument is locktime value as yyyy-mm. For example `2021-03`.
(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.
(createwatchonly) Create a watch-only fidelity bond wallet.
(createwatchonly) Create a watch-only wallet.
(createfbwatchonly) Create a watch-only fidelity bond wallet.
(setlabel) Set the label associated with the given address.
"""
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method] [args..]',
Expand Down Expand Up @@ -270,7 +272,7 @@ def __init__(self, wallet_path_repr, account, address_type, branchentries=None,
FidelityBondMixin.BIP32_BURN_ID]
self.address_type = address_type
if xpub:
assert xpub.startswith('xpub') or xpub.startswith('tpub')
assert xpub.startswith('xpub') or xpub.startswith('tpub') or xpub.startswith('ypub') or xpub.startswith('zpub')
self.xpub = xpub if xpub else ""
self.branchentries = branchentries

Expand Down Expand Up @@ -1390,7 +1392,7 @@ def wallet_addtxoutproof(wallet_service, hdpath, txoutproof):
new_merkle_branch, block_index)
return "Done"

def wallet_createwatchonly(wallet_root_path, master_pub_key):
def wallet_createwatchonly(wallet_root_path, master_pub_key, is_fidelity_bond_wallet = False):

wallet_name = cli_get_wallet_file_name(defaultname="watchonly.jmdat")
if not wallet_name:
Expand All @@ -1401,17 +1403,39 @@ def wallet_createwatchonly(wallet_root_path, master_pub_key):

password = cli_get_wallet_passphrase_check()
if not password:
jmprint("The passphrase can not be empty", "error")
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 ""
if is_fidelity_bond_wallet:
entropy = FidelityBondMixin.get_xpub_from_fidelity_bond_master_pub_key(master_pub_key)
if not entropy:
jmprint("Error with provided master public key", "error")
return ""
else:
entropy = master_pub_key
entropy = entropy.encode()

wallet = create_wallet(wallet_path, password,
max_mixdepth=FidelityBondMixin.FIDELITY_BOND_MIXDEPTH,
wallet_cls=FidelityBondWatchonlyWallet, entropy=entropy)
if is_fidelity_bond_wallet:
create_wallet(wallet_path, password,
max_mixdepth=FidelityBondMixin.FIDELITY_BOND_MIXDEPTH,
wallet_cls=FidelityBondWatchonlyWallet, entropy=entropy)
else:
if master_pub_key.startswith('zpub'):
wallet_cls = SegwitWatchonlyWallet
elif master_pub_key.startswith('ypub'):
wallet_cls = SegwitLegacyWatchonlyWallet
else:
if is_native_segwit_mode():
wallet_cls = SegwitWatchonlyWallet
elif is_segwit_mode():
wallet_cls = SegwitLegacyWatchonlyWallet
else:
jmprint("Only segwit wallets are supported for watch only mode", "error")
return ""

create_wallet(wallet_path, password,
max_mixdepth=WatchonlyMixin.WATCH_ONLY_MIXDEPTH,
wallet_cls=wallet_cls, entropy=entropy)
return "Done"

def get_configured_wallet_type(support_fidelity_bonds):
Expand Down Expand Up @@ -1583,7 +1607,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', 'createwatchonly']
noseed_methods = ['generate', 'recover', 'createwatchonly', 'createfbwatchonly']
methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey',
'history', 'showutxos', 'freeze', 'gettimelockaddress',
'addtxoutproof', 'changepass', 'setlabel']
Expand Down Expand Up @@ -1706,6 +1730,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 == "createfbwatchonly":
if len(args) < 2:
jmprint("args: [master public key]", "error")
sys.exit(EXIT_ARGERROR)
return wallet_createwatchonly(wallet_root_path, args[1], is_fidelity_bond_wallet=True)
elif method == "createwatchonly":
if len(args) < 2:
jmprint("args: [master public key]", "error")
Expand Down
Loading