diff --git a/docs/fidelity-bonds.md b/docs/fidelity-bonds.md index 9fa88ba7d..2ad12b662 100644 --- a/docs/fidelity-bonds.md +++ b/docs/fidelity-bonds.md @@ -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: diff --git a/src/jmbitcoin/secp256k1_deterministic.py b/src/jmbitcoin/secp256k1_deterministic.py index eb4fa3dbd..45e18baff 100644 --- a/src/jmbitcoin/secp256k1_deterministic.py +++ b/src/jmbitcoin/secp256k1_deterministic.py @@ -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 diff --git a/src/jmclient/cryptoengine.py b/src/jmclient/cryptoengine.py index 065e3eeac..ea4a75a60 100644 --- a/src/jmclient/cryptoengine.py +++ b/src/jmclient/cryptoengine.py @@ -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} @@ -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 @@ -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 diff --git a/src/jmclient/wallet.py b/src/jmclient/wallet.py index 4f29d081b..ab3cc536b 100644 --- a/src/jmclient/wallet.py +++ b/src/jmclient/wallet.py @@ -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 @@ -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): @@ -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 @@ -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 } diff --git a/src/jmclient/wallet_utils.py b/src/jmclient/wallet_utils.py index 8956e7331..348e6be6d 100644 --- a/src/jmclient/wallet_utils.py +++ b/src/jmclient/wallet_utils.py @@ -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, @@ -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..]', @@ -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 @@ -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: @@ -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): @@ -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'] @@ -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")