Skip to content

Commit

Permalink
Create SegwitWatchonlyWallet and SegwitLegacyWatchonlyWallet
Browse files Browse the repository at this point in the history
Renamed the original createwatchonly command to createfbwatchonly, and
repurposed the createwatchonly command to instead create a general
purpose watch only wallet.

Co-authored-by: wukong1971 <[email protected]>
  • Loading branch information
kristapsk and BitcoinWukong committed Feb 20, 2024
1 parent 7b31e61 commit 4ad2ada
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 22 deletions.
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

0 comments on commit 4ad2ada

Please sign in to comment.