diff --git a/src/jmclient/cryptoengine.py b/src/jmclient/cryptoengine.py index ce9b77c..2ce2757 100644 --- a/src/jmclient/cryptoengine.py +++ b/src/jmclient/cryptoengine.py @@ -19,7 +19,8 @@ from .configure import get_network, jm_single 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, TYPE_P2TR_FROST, TYPE_TAPROOT_WALLET_FIDELITY_BONDS = range(13) + TYPE_P2TR, TYPE_P2TR_FROST, TYPE_TAPROOT_WALLET_FIDELITY_BONDS, \ + TYPE_TAPROOT_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_P2TR, = range(15) NET_MAINNET, NET_TESTNET, NET_SIGNET = range(3) NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET, 'signet': NET_SIGNET} @@ -504,6 +505,34 @@ class BTC_P2TR(BTCEngine): spent_outputs=spent_outputs) +class BTC_Watchonly_P2TR(BTC_P2TR): + + @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_P2TR, cls).derive_bip32_pub_export( + master_key, BTC_Watchonly_Timelocked_P2WSH.get_watchonly_path(path)) + + @classmethod + async def sign_transaction(cls, tx, index, privkey, amount, + hashcode=btc.SIGHASH_ALL, **kwargs): + raise RuntimeError("Cannot spend from watch-only wallets") + + class BTC_P2TR_FROST(BTC_P2TR): @classmethod @@ -526,11 +555,17 @@ ENGINES = { TYPE_P2PKH: BTC_P2PKH, TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH, TYPE_P2WPKH: BTC_P2WPKH, + TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH, TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH, + TYPE_WATCHONLY_P2WPKH: BTC_Watchonly_P2WPKH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS: BTC_P2WPKH, + TYPE_P2TR: BTC_P2TR, - TYPE_P2TR_FROST: BTC_P2TR_FROST, + + TYPE_WATCHONLY_P2TR: BTC_Watchonly_P2TR, TYPE_TAPROOT_WALLET_FIDELITY_BONDS: BTC_P2TR, + + TYPE_P2TR_FROST: BTC_P2TR_FROST, } diff --git a/src/jmclient/wallet.py b/src/jmclient/wallet.py index b21d986..a94670f 100644 --- a/src/jmclient/wallet.py +++ b/src/jmclient/wallet.py @@ -30,7 +30,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, TYPE_P2TR_FROST, ENGINES, \ - detect_script_type, EngineError, TYPE_TAPROOT_WALLET_FIDELITY_BONDS + detect_script_type, EngineError, TYPE_TAPROOT_WALLET_FIDELITY_BONDS, \ + TYPE_TAPROOT_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_P2TR from .storage import DKGRecoveryStorage from .support import get_random_bytes from . import mn_encode, mn_decode @@ -870,7 +871,7 @@ class BaseWallet(object): elif self.TYPE == TYPE_P2SH_P2WPKH: return 'p2sh-p2wpkh' elif self.TYPE in (TYPE_P2TR, TYPE_TAPROOT_WALLET_FIDELITY_BONDS, - TYPE_P2TR_FROST): + TYPE_P2TR_FROST, TYPE_WATCHONLY_P2TR): return 'p2tr' elif self.TYPE in (TYPE_P2WPKH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS): @@ -3854,12 +3855,72 @@ class FidelityBondWatchonlyWallet(FidelityBondMixin, BIP84Wallet): return pubkey, self._ENGINE +class TaprootFidelityBondWatchonlyWallet(FidelityBondMixin, BIP86Wallet): + TYPE = TYPE_TAPROOT_WATCHONLY_FIDELITY_BONDS + _ENGINE = ENGINES[TYPE_WATCHONLY_P2TR] + _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()._get_bip32_export_path(mixdepth, address_type) + return path + + def _get_key_from_path(self, path, + validate_cache: bool = False): + raise WalletCannotGetPrivateKeyFromWatchOnly() + + async def _get_keypair_from_path(self, path, + validate_cache: bool = False): + raise WalletCannotGetPrivateKeyFromWatchOnly() + + async def _get_pubkey_from_path(self, path, + validate_cache: bool = False): + if not self._is_my_bip32_path(path): + return await super()._get_pubkey_from_path(path, + validate_cache=validate_cache) + if self.is_timelocked_path(path): + key_path = path[:-1] + locktime = path[-1] + cache = self._get_cache_for_path(key_path) + pubkey = cache.get(b'P') + if pubkey is None or validate_cache: + new_pubkey = self._TIMELOCK_ENGINE.derive_bip32_privkey( + self._master_key, key_path) + if pubkey is None: + cache[b'P'] = pubkey = new_pubkey + elif pubkey != new_pubkey: + raise WalletCacheValidationFailed() + return (pubkey, locktime), self._TIMELOCK_ENGINE + cache = self._get_cache_for_path(path) + pubkey = cache.get(b'P') + if pubkey is None or validate_cache: + new_pubkey = self._ENGINE.derive_bip32_privkey( + self._master_key, path) + if pubkey is None: + cache[b'P'] = pubkey = new_pubkey + elif pubkey != new_pubkey: + raise WalletCacheValidationFailed() + return pubkey, self._ENGINE + + WALLET_IMPLEMENTATIONS = { LegacyWallet.TYPE: LegacyWallet, SegwitLegacyWallet.TYPE: SegwitLegacyWallet, + SegwitWallet.TYPE: SegwitWallet, SegwitWalletFidelityBonds.TYPE: SegwitWalletFidelityBonds, FidelityBondWatchonlyWallet.TYPE: FidelityBondWatchonlyWallet, + TaprootWallet.TYPE: TaprootWallet, + TaprootWalletFidelityBonds.TYPE: TaprootWalletFidelityBonds, + TaprootFidelityBondWatchonlyWallet.TYPE: TaprootFidelityBondWatchonlyWallet, + FrostWallet.TYPE: FrostWallet, } diff --git a/src/jmclient/wallet_utils.py b/src/jmclient/wallet_utils.py index 1b2ab95..5b9d68b 100644 --- a/src/jmclient/wallet_utils.py +++ b/src/jmclient/wallet_utils.py @@ -33,8 +33,10 @@ from jmbase.support import (get_password, jmprint, EXIT_FAILURE, from jmfrost.chilldkg_ref.chilldkg import hostpubkey_gen from .frost_clients import FROSTClient from .frost_ipc import FrostIPCServer, FrostIPCClient -from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, \ - TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_P2TR, TYPE_P2TR_FROST +from .cryptoengine import ( + TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, + TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_P2TR, TYPE_P2TR_FROST, + TYPE_TAPROOT_WALLET_FIDELITY_BONDS) from .output import fmt_utxo import jmbitcoin as btc from .descriptor import descsum_create @@ -1495,7 +1497,7 @@ def get_configured_wallet_type(support_fidelity_bonds): if is_frost_mode(): return TYPE_P2TR_FROST if is_taproot_mode(): - return TYPE_P2TR + configured_type = TYPE_P2TR elif is_segwit_mode(): if is_native_segwit_mode(): configured_type = TYPE_P2WPKH @@ -1507,6 +1509,8 @@ def get_configured_wallet_type(support_fidelity_bonds): if configured_type == TYPE_P2WPKH: return TYPE_SEGWIT_WALLET_FIDELITY_BONDS + elif configured_type == TYPE_P2TR: + return TYPE_TAPROOT_WALLET_FIDELITY_BONDS else: raise ValueError("Fidelity bonds not supported with the configured " "options of segwit and native. Edit joinmarket.cfg") diff --git a/test/jmclient/test_payjoin.py b/test/jmclient/test_payjoin.py index 30c4b99..401bc45 100644 --- a/test/jmclient/test_payjoin.py +++ b/test/jmclient/test_payjoin.py @@ -10,7 +10,6 @@ from twisted.internet import reactor, defer from twisted.web.server import Site, NOT_DONE_YET from twisted.web.client import readBody from twisted.web.http_headers import Headers -from twisted.trial import unittest import urllib.parse as urlparse from urllib.parse import urlencode