diff --git a/.gitignore b/.gitignore index 3a5b40b..e62caca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ __pycache__ /*.eggs .tox .eggs +.ruff_cache +.pytest_cache *~ # geckodriver.log @@ -15,4 +17,4 @@ __pycache__ docker/.env # vscode dev container settings -compose-dev.yaml \ No newline at end of file +compose-dev.yaml diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 8cb01d9..307e6ee 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1358,7 +1358,9 @@ class BasicSwap(BaseApp): legacy_root_hash = ci.getSeedHash(root_key, 20) self.setStringKV(key_str, legacy_root_hash.hex(), cursor) - def initialiseWallet(self, interface_type, raise_errors: bool = False) -> None: + def initialiseWallet( + self, interface_type, raise_errors: bool = False, restore_time: int = -1 + ) -> None: if interface_type == Coins.PART: return ci = self.ci(interface_type) @@ -1377,7 +1379,7 @@ class BasicSwap(BaseApp): root_key = self.getWalletKey(interface_type, 1) try: - ci.initialiseWallet(root_key) + ci.initialiseWallet(root_key, restore_time) except Exception as e: # < 0.21: sethdseed cannot set a new HD seed while still in Initial Block Download. self.log.error(f"initialiseWallet failed: {e}") diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index e026d20..0efd9cd 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -182,6 +182,7 @@ BSX_UPDATE_UNMANAGED = toBool( UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700)) UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700)) COINS_RPCBIND_IP = os.getenv("COINS_RPCBIND_IP", "127.0.0.1") +DEFAULT_RESTORE_TIME = int(os.getenv("DEFAULT_RESTORE_TIME", 1577833261)) # 2020 PART_ZMQ_PORT = int(os.getenv("PART_ZMQ_PORT", 20792)) PART_RPC_HOST = os.getenv("PART_RPC_HOST", "127.0.0.1") @@ -1707,6 +1708,11 @@ def printHelp(): DEFAULT_WOW_RESTORE_HEIGHT ) ) + print( + "--walletrestoretime=n Time to restore wallets from, default:{}, -1 for now.".format( + DEFAULT_RESTORE_TIME + ) + ) print( "--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false" ) @@ -1808,12 +1814,18 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy): def encrypt_wallet(swap_client, coin_type) -> None: ci = swap_client.ci(coin_type) - ci.changeWalletPassword("", WALLET_ENCRYPTION_PWD) + ci.changeWalletPassword("", WALLET_ENCRYPTION_PWD, check_seed_if_encrypt=False) ci.unlockWallet(WALLET_ENCRYPTION_PWD) def initialise_wallets( - particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy + particl_wallet_mnemonic, + with_coins, + data_dir, + settings, + chain, + use_tor_proxy, + extra_opts={}, ): swap_client = None daemons = [] @@ -1922,7 +1934,7 @@ def initialise_wallets( if WALLET_ENCRYPTION_PWD == "" else WALLET_ENCRYPTION_PWD ) - extra_opts = [ + extra_args = [ '--appdata="{}"'.format(coin_settings["datadir"]), "--pass={}".format(dcr_password), ] @@ -1931,7 +1943,7 @@ def initialise_wallets( args = [ os.path.join(coin_settings["bindir"], filename), "--create", - ] + extra_opts + ] + extra_args hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex() createDCRWallet(args, hex_seed, logger, threading.Event()) continue @@ -2028,6 +2040,7 @@ def initialise_wallets( ) for coin_name in with_coins: + coin_settings = settings["chainclients"][coin_name] c = swap_client.getCoinIdFromName(coin_name) if c in (Coins.PART,): continue @@ -2035,14 +2048,29 @@ def initialise_wallets( # initialiseWallet only sets main_wallet_seedid_ swap_client.waitForDaemonRPC(c) try: - swap_client.initialiseWallet(c, raise_errors=True) + default_restore_time = ( + -1 if generated_mnemonic else DEFAULT_RESTORE_TIME + ) # Set to -1 (now) if key is newly generated + restore_time: int = extra_opts.get( + "walletrestoretime", default_restore_time + ) + + swap_client.initialiseWallet( + c, raise_errors=True, restore_time=restore_time + ) + if c not in (Coins.XMR, Coins.WOW): + if restore_time == -1: + restore_time = int(time.time()) + coin_settings["restore_time"] = restore_time except Exception as e: coins_failed_to_initialise.append((c, e)) if WALLET_ENCRYPTION_PWD != "" and ( c not in coins_to_create_wallets_for or c in (Coins.DASH,) ): # TODO: Remove DASH workaround try: - swap_client.ci(c).changeWalletPassword("", WALLET_ENCRYPTION_PWD) + swap_client.ci(c).changeWalletPassword( + "", WALLET_ENCRYPTION_PWD, check_seed_if_encrypt=False + ) except Exception as e: # noqa: F841 logger.warning(f"changeWalletPassword failed for {coin_name}.") @@ -2363,6 +2391,9 @@ def main(): if name == "wowrestoreheight": wow_restore_height = int(s[1]) continue + if name == "walletrestoretime": + extra_opts["walletrestoretime"] = int(s[1]) + continue if name == "keysdirpath": extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"')) continue @@ -2823,6 +2854,7 @@ def main(): settings, chain, use_tor_proxy, + extra_opts=extra_opts, ) print("Done.") @@ -2944,6 +2976,7 @@ def main(): settings, chain, use_tor_proxy, + extra_opts=extra_opts, ) save_config(config_path, settings) @@ -3082,15 +3115,21 @@ def main(): for c in with_coins: prepareDataDir(c, settings, chain, particl_wallet_mnemonic, extra_opts) - save_config(config_path, settings) - if particl_wallet_mnemonic == "none": + save_config(config_path, settings) logger.info("Done.") return 0 initialise_wallets( - particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy + particl_wallet_mnemonic, + with_coins, + data_dir, + settings, + chain, + use_tor_proxy, + extra_opts=extra_opts, ) + save_config(config_path, settings) print("Done.") diff --git a/basicswap/interface/bch.py b/basicswap/interface/bch.py index 074ed35..a5f09b4 100644 --- a/basicswap/interface/bch.py +++ b/basicswap/interface/bch.py @@ -106,6 +106,31 @@ class BCHInterface(BTCInterface): ) + self.make_int(u["amount"], r=1) return unspent_addr + def createWallet(self, wallet_name: str, password: str = ""): + self.rpc("createwallet", [wallet_name, False]) + if password != "": + self.rpc( + "encryptwallet", + [ + password, + ], + override_wallet=wallet_name, + ) + + def newKeypool(self) -> None: + self._log.debug("Refreshing keypool.") + + # Use up current keypool + wi = self.rpc_wallet("getwalletinfo") + keypool_size: int = wi["keypoolsize"] + for i in range(keypool_size): + _ = self.rpc_wallet("getnewaddress") + keypoolsize_hd_internal: int = wi["keypoolsize_hd_internal"] + for i in range(keypoolsize_hd_internal): + _ = self.rpc_wallet("getrawchangeaddress") + + self.rpc_wallet("keypoolrefill") + # returns pkh def decodeAddress(self, address: str) -> bytes: return bytes(Address.from_string(address).payload) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index d65702b..04abd5f 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -10,8 +10,13 @@ import base64 import hashlib import json import logging +import mmap +import os +import shutil +import sqlite3 import traceback + from io import BytesIO from basicswap.basicswap_util import ( @@ -377,7 +382,7 @@ class BTCInterface(Secp256k1Interface): last_block_header = prev_block_header raise ValueError(f"Block header not found at time: {time}") - def initialiseWallet(self, key_bytes: bytes) -> None: + def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None: assert len(key_bytes) == 32 self._have_checked_seed = False if self._use_descriptors: @@ -387,6 +392,7 @@ class BTCInterface(Secp256k1Interface): ek_encoded: str = self.encode_secret_extkey(ek.encode_v()) desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)") desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)") + rv = self.rpc_wallet( "importdescriptors", [ @@ -394,7 +400,7 @@ class BTCInterface(Secp256k1Interface): {"desc": desc_external, "timestamp": "now", "active": True}, { "desc": desc_internal, - "timestamp": "now", + "timestamp": "now" if restore_time == -1 else restore_time, "active": True, "internal": True, }, @@ -411,7 +417,18 @@ class BTCInterface(Secp256k1Interface): raise ValueError("Failed to import descriptors.") else: key_wif = self.encodeKey(key_bytes) - self.rpc_wallet("sethdseed", [True, key_wif]) + try: + self.rpc_wallet("sethdseed", [True, key_wif]) + except Exception as e: + self._log.debug(f"sethdseed failed: {e}") + """ + # TODO: Find derived key counts + if "Already have this key" in str(e): + key_id: bytes = self.getSeedHash(key_bytes) + self.setActiveKeyChain(key_id) + else: + """ + raise (e) def getWalletInfo(self): rv = self.rpc_wallet("getwalletinfo") @@ -455,10 +472,6 @@ class BTCInterface(Secp256k1Interface): self.close_rpc(rpc_conn) raise ValueError(f"{self.coin_name()} wallet restore height not found.") - def getWalletSeedID(self) -> str: - wi = self.rpc_wallet("getwalletinfo") - return "Not found" if "hdseedid" not in wi else wi["hdseedid"] - def getActiveDescriptor(self): descriptors = self.rpc_wallet("listdescriptors")["descriptors"] for descriptor in descriptors: @@ -470,21 +483,24 @@ class BTCInterface(Secp256k1Interface): return descriptor return None - def checkExpectedSeed(self, expect_seedid: str) -> bool: + def getWalletSeedID(self) -> str: if self._use_descriptors: descriptor = self.getActiveDescriptor() if descriptor is None: self._log.debug("Could not find active descriptor.") - return False - + return "Not found" end = descriptor["desc"].find("/") if end < 10: - return False + return "Not found" extkey = descriptor["desc"][5:end] extkey_data = b58decode(extkey)[4:-4] extkey_data_hash: bytes = hash160(extkey_data) - return True if extkey_data_hash.hex() == expect_seedid else False + return extkey_data_hash.hex() + wi = self.rpc_wallet("getwalletinfo") + return "Not found" if "hdseedid" not in wi else wi["hdseedid"] + + def checkExpectedSeed(self, expect_seedid: str) -> bool: wallet_seed_id = self.getWalletSeedID() self._expect_seedid_hex = expect_seedid self._have_checked_seed = True @@ -1978,12 +1994,234 @@ class BTCInterface(Secp256k1Interface): locked = encrypted and wallet_info["unlocked_until"] <= 0 return encrypted, locked - def changeWalletPassword(self, old_password: str, new_password: str): + def createWallet(self, wallet_name: str, password: str = "") -> None: + self.rpc( + "createwallet", + [wallet_name, False, True, password, False, self._use_descriptors], + ) + + def setActiveWallet(self, wallet_name: str) -> None: + # For debugging + self.rpc_wallet = make_rpc_func( + self._rpcport, self._rpcauth, host=self._rpc_host, wallet=wallet_name + ) + self._rpc_wallet = wallet_name + + def newKeypool(self) -> None: + self._log.debug("Running newkeypool.") + self.rpc_wallet("newkeypool") + + def encryptWallet(self, password: str, check_seed: bool = True): + # Watchonly wallets are not encrypted + # Workaround for https://github.com/bitcoin/bitcoin/issues/26607 + seed_id_before: str = self.getWalletSeedID() + orig_active_descriptors = [] + orig_hdchain_bytes = None + walletpath = None + max_hdchain_key_count: int = 4000000 # Arbitrary + + chain_client_settings = self._sc.getChainClientSettings( + self.coin_type() + ) # basicswap.json + if ( + chain_client_settings.get("manage_daemon", False) + and check_seed is True + and seed_id_before != "Not found" + ): + # Store active keys + self.rpc("unloadwallet", [self._rpc_wallet]) + + datadir = chain_client_settings["datadir"] + if self._network != "mainnet": + datadir = os.path.join(datadir, self._network) + try_wallet_path = os.path.join(datadir, self._rpc_wallet) + if os.path.exists(try_wallet_path): + walletpath = try_wallet_path + else: + try_wallet_path = os.path.join(datadir, "wallets", self._rpc_wallet) + if os.path.exists(try_wallet_path): + walletpath = try_wallet_path + + walletfilepath = walletpath + if os.path.isdir(walletpath): + walletfilepath = os.path.join(walletpath, "wallet.dat") + + if walletpath is None: + self._log.warning(f"Unable to find {self.ticker()} wallet path.") + else: + if self._use_descriptors: + orig_active_descriptors = [] + with sqlite3.connect(walletfilepath) as conn: + c = conn.cursor() + rows = c.execute( + "SELECT * FROM main WHERE key in (:kext, :kint)", + { + "kext": bytes.fromhex( + "1161637469766565787465726e616c73706b02" + ), + "kint": bytes.fromhex( + "11616374697665696e7465726e616c73706b02" + ), + }, + ) + for row in rows: + k, v = row + orig_active_descriptors.append({"k": k, "v": v}) + else: + seedid_bytes: bytes = bytes.fromhex(seed_id_before)[::-1] + with open(walletfilepath, "rb") as fp: + with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as mm: + pos = mm.find(seedid_bytes) + while pos != -1: + mm.seek(pos - 8) + hdchain_bytes = mm.read(12 + 20) + version = int.from_bytes(hdchain_bytes[:4], "little") + if version == 2: + external_counter = int.from_bytes( + hdchain_bytes[4:8], "little" + ) + internal_counter = int.from_bytes( + hdchain_bytes[-4:], "little" + ) + if ( + external_counter > 0 + and external_counter <= max_hdchain_key_count + and internal_counter > 0 + and internal_counter <= max_hdchain_key_count + ): + orig_hdchain_bytes = hdchain_bytes + self._log.debug( + f"Found hdchain for: {seed_id_before} external_counter: {external_counter}, internal_counter: {internal_counter}." + ) + break + pos = mm.find(seedid_bytes, pos + 1) + + self.rpc("loadwallet", [self._rpc_wallet]) + + self.rpc_wallet("encryptwallet", [password]) + + if check_seed is False or seed_id_before == "Not found" or walletpath is None: + return + seed_id_after: str = self.getWalletSeedID() + + if seed_id_before == seed_id_after: + return + self._log.warning(f"{self.ticker()} wallet seed changed after encryption.") + self._log.debug( + f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}." + ) + self.setWalletSeedWarning(True) + + if chain_client_settings.get("manage_daemon", False) is False: + self._log.warning( + f"{self.ticker()} manage_daemon is false. Can't attempt to fix." + ) + return + if self._use_descriptors: + if len(orig_active_descriptors) < 2: + self._log.error( + "Could not find original active descriptors for wallet." + ) + return + self._log.info("Attempting to revert to last descriptors.") + else: + if orig_hdchain_bytes is None: + self._log.error("Could not find hdchain for wallet.") + return + self._log.info("Attempting to revert to last hdchain.") + try: + # Make a copy of the encrypted wallet before modifying it + bkp_path = walletpath + ".bkp" + for i in range(100): + if not os.path.exists(bkp_path): + break + bkp_path = walletpath + f".bkp{i}" + + if os.path.exists(bkp_path): + self._log.error("Could not find backup path for wallet.") + return + + self.rpc("unloadwallet", [self._rpc_wallet]) + + if os.path.isfile(walletpath): + shutil.copy(walletpath, bkp_path) + else: + shutil.copytree(walletpath, bkp_path) + + hdchain_replaced: bool = False + if self._use_descriptors: + with sqlite3.connect(walletfilepath) as conn: + c = conn.cursor() + c.executemany( + "UPDATE main SET value = :v WHERE key = :k", + orig_active_descriptors, + ) + conn.commit() + else: + seedid_after_bytes: bytes = bytes.fromhex(seed_id_after)[::-1] + with open(walletfilepath, "r+b") as fp: + with mmap.mmap(fp.fileno(), 0) as mm: + pos = mm.find(seedid_after_bytes) + while pos != -1: + mm.seek(pos - 8) + hdchain_bytes = mm.read(12 + 20) + version = int.from_bytes(hdchain_bytes[:4], "little") + if version == 2: + external_counter = int.from_bytes( + hdchain_bytes[4:8], "little" + ) + internal_counter = int.from_bytes( + hdchain_bytes[-4:], "little" + ) + if ( + external_counter > 0 + and external_counter <= max_hdchain_key_count + and internal_counter > 0 + and internal_counter <= max_hdchain_key_count + ): + self._log.debug( + f"Replacing hdchain for: {seed_id_after} external_counter: {external_counter}, internal_counter: {internal_counter}." + ) + offset: int = pos - 8 + mm.seek(offset) + mm.write(orig_hdchain_bytes) + self._log.debug( + f"hdchain replaced at offset: {offset}." + ) + hdchain_replaced = True + # Can appear multiple times in file, replace all. + pos = mm.find(seedid_after_bytes, pos + 1) + + if hdchain_replaced is False: + self._log.error("Could not find new hdchain in wallet.") + + self.rpc("loadwallet", [self._rpc_wallet]) + + if hdchain_replaced: + self.unlockWallet(password, check_seed=False) + seed_id_after_restore: str = self.getWalletSeedID() + if seed_id_after_restore == seed_id_before: + self.newKeypool() + else: + self._log.warning( + f"Expected seed id not found: {seed_id_before}, have {seed_id_after_restore}." + ) + + self.lockWallet() + + except Exception as e: + self._log.error(f"{self.ticker()} recreating wallet failed: {e}.") + if self._sc.debug: + self._log.error(traceback.format_exc()) + + def changeWalletPassword( + self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True + ): self._log.info("changeWalletPassword - {}".format(self.ticker())) if old_password == "": if self.isWalletEncrypted(): raise ValueError("Old password must be set") - return self.rpc_wallet("encryptwallet", [new_password]) + return self.encryptWallet(new_password, check_seed=check_seed_if_encrypt) self.rpc_wallet("walletpassphrasechange", [old_password, new_password]) def unlockWallet(self, password: str, check_seed: bool = True) -> None: @@ -2001,9 +2239,16 @@ class BTCInterface(Secp256k1Interface): ) # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors self.rpc( - "createwallet", [self._rpc_wallet, False, True, "", False, False] + "createwallet", + [ + self._rpc_wallet, + False, + True, + password, + False, + self._use_descriptors, + ], ) - self.rpc_wallet("encryptwallet", [password]) # Max timeout value, ~3 years self.rpc_wallet("walletpassphrase", [password, 100000000]) diff --git a/basicswap/interface/dash.py b/basicswap/interface/dash.py index 5904c17..72517e1 100644 --- a/basicswap/interface/dash.py +++ b/basicswap/interface/dash.py @@ -47,7 +47,7 @@ class DASHInterface(BTCInterface): def entropyToMnemonic(self, key: bytes) -> None: return Mnemonic("english").to_mnemonic(key) - def initialiseWallet(self, key_bytes: bytes) -> None: + def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None: self._have_checked_seed = False if self._wallet_v20_compatible: self._log.warning("Generating wallet compatible with v20 seed.") @@ -66,7 +66,11 @@ class DASHInterface(BTCInterface): def checkExpectedSeed(self, expect_seedid: str) -> bool: self._expect_seedid_hex = expect_seedid - rv = self.rpc_wallet("dumphdinfo") + try: + rv = self.rpc_wallet("dumphdinfo") + except Exception as e: + self._log.debug(f"DASH dumphdinfo failed {e}.") + return False if rv["mnemonic"] != "": entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" ")) entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex() @@ -120,3 +124,36 @@ class DASHInterface(BTCInterface): def lockWallet(self): super().lockWallet() self._wallet_passphrase = "" + + def encryptWallet( + self, old_password: str, new_password: str, check_seed: bool = True + ): + if old_password != "": + self.unlockWallet(old_password, check_seed=False) + seed_id_before: str = self.getWalletSeedID() + + self.rpc_wallet("encryptwallet", [new_password]) + + if check_seed is False or seed_id_before == "Not found": + return + self.unlockWallet(new_password, check_seed=False) + seed_id_after: str = self.getWalletSeedID() + + self.lockWallet() + if seed_id_before == seed_id_after: + return + self._log.warning(f"{self.ticker()} wallet seed changed after encryption.") + self._log.debug( + f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}." + ) + self.setWalletSeedWarning(True) + + def changeWalletPassword( + self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True + ): + self._log.info("changeWalletPassword - {}".format(self.ticker())) + if old_password == "": + if self.isWalletEncrypted(): + raise ValueError("Old password must be set") + return self.encryptWallet(old_password, new_password, check_seed_if_encrypt) + self.rpc_wallet("walletpassphrasechange", [old_password, new_password]) diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index c59c547..9bd2909 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -332,14 +332,14 @@ class DCRInterface(Secp256k1Interface): def testDaemonRPC(self, with_wallet=True) -> None: if with_wallet: - self.rpc_wallet("getinfo") + self.rpc_wallet("walletislocked") else: self.rpc("getblockchaininfo") def getChainHeight(self) -> int: return self.rpc("getblockcount") - def initialiseWallet(self, key: bytes) -> None: + def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None: # Load with --create pass @@ -354,7 +354,9 @@ class DCRInterface(Secp256k1Interface): walletislocked = self.rpc_wallet("walletislocked") return True, walletislocked - def changeWalletPassword(self, old_password: str, new_password: str): + def changeWalletPassword( + self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True + ): self._log.info("changeWalletPassword - {}".format(self.ticker())) if old_password == "": # Read initial pwd from settings diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index 33d9c1b..bb56292 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -51,13 +51,32 @@ class FIROInterface(BTCInterface): def getExchangeName(self, exchange_name: str) -> str: return "zcoin" - def initialiseWallet(self, key): + def initialiseWallet(self, key, restore_time: int = -1): # load with -hdseed= parameter pass def checkWallets(self) -> int: return 1 + def encryptWallet(self, password: str, check_seed: bool = True): + # Watchonly wallets are not encrypted + # Firo shuts down after encryptwallet + seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found" + + self.rpc_wallet("encryptwallet", [password]) + + if check_seed is False or seed_id_before == "Not found": + return + seed_id_after: str = self.getWalletSeedID() + + if seed_id_before == seed_id_after: + return + self._log.warning(f"{self.ticker()} wallet seed changed after encryption.") + self._log.debug( + f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}." + ) + self.setWalletSeedWarning(True) + def getNewAddress(self, use_segwit, label="swap_receive"): return self.rpc("getnewaddress", [label]) # addr_plain = self.rpc('getnewaddress', [label]) diff --git a/basicswap/interface/nav.py b/basicswap/interface/nav.py index 0cf322e..13b2f6c 100644 --- a/basicswap/interface/nav.py +++ b/basicswap/interface/nav.py @@ -87,7 +87,7 @@ class NAVInterface(BTCInterface): # p2sh-p2wsh return True - def initialiseWallet(self, key): + def initialiseWallet(self, key, restore_time: int = -1): # Load with -importmnemonic= parameter pass diff --git a/basicswap/interface/part.py b/basicswap/interface/part.py index 59e3a2d..10c481c 100644 --- a/basicswap/interface/part.py +++ b/basicswap/interface/part.py @@ -110,7 +110,7 @@ class PARTInterface(BTCInterface): ) return index_info["spentindex"] - def initialiseWallet(self, key: bytes) -> None: + def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None: raise ValueError("TODO") def withdrawCoin(self, value, addr_to, subfee): diff --git a/basicswap/interface/pivx.py b/basicswap/interface/pivx.py index e28993d..2c7017f 100644 --- a/basicswap/interface/pivx.py +++ b/basicswap/interface/pivx.py @@ -34,6 +34,35 @@ class PIVXInterface(BTCInterface): self._rpcport, self._rpcauth, host=self._rpc_host ) + def encryptWallet(self, password: str, check_seed: bool = True): + # Watchonly wallets are not encrypted + + seed_id_before: str = self.getWalletSeedID() + + self.rpc_wallet("encryptwallet", [password]) + + if check_seed is False or seed_id_before == "Not found": + return + seed_id_after: str = self.getWalletSeedID() + + if seed_id_before == seed_id_after: + return + self._log.warning(f"{self.ticker()} wallet seed changed after encryption.") + self._log.debug( + f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}." + ) + self.setWalletSeedWarning(True) + # Workaround for https://github.com/bitcoin/bitcoin/issues/26607 + chain_client_settings = self._sc.getChainClientSettings( + self.coin_type() + ) # basicswap.json + + if chain_client_settings.get("manage_daemon", False) is False: + self._log.warning( + f"{self.ticker()} manage_daemon is false. Can't attempt to fix." + ) + return + def signTxWithWallet(self, tx): rv = self.rpc("signrawtransaction", [tx.hex()]) return bytes.fromhex(rv["hex"]) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index cf6ce73..71f4880 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -766,7 +766,9 @@ class XMRInterface(CoinInterface): balance_info = self.rpc_wallet("get_balance") return balance_info["unlocked_balance"] - def changeWalletPassword(self, old_password, new_password): + def changeWalletPassword( + self, old_password, new_password, check_seed_if_encrypt: bool = True + ): self._log.info("changeWalletPassword - {}".format(self.ticker())) orig_password = self._wallet_password if old_password != "": diff --git a/basicswap/js_server.py b/basicswap/js_server.py index af470f6..70d6453 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -850,7 +850,12 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes: swap_client.checkSystemStatus() post_data = getFormData(post_string, is_json) - coin = getCoinType(get_data_entry(post_data, "coin")) + coin_in = get_data_entry(post_data, "coin") + try: + coin = getCoinIdFromName(coin_in) + except Exception: + coin = getCoinType(coin_in) + if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND): raise ValueError("Particl wallet seed is set from the Basicswap mnemonic.") @@ -878,12 +883,17 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes: expect_seedid = swap_client.getStringKV( "main_wallet_seedid_" + ci.coin_name().lower() ) + try: + wallet_seed_id = ci.getWalletSeedID() + except Exception as e: + wallet_seed_id = f"Error: {e}" rv.update( { "seed": seed_key.hex(), "seed_id": seed_id.hex(), "expected_seed_id": "Unset" if expect_seedid is None else expect_seedid, + "current_seed_id": wallet_seed_id, } ) diff --git a/basicswap/rpc.py b/basicswap/rpc.py index 43b4b46..49eff25 100644 --- a/basicswap/rpc.py +++ b/basicswap/rpc.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert +# Copyright (c) 2025 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. -import os import json -import shlex -import urllib import traceback -import subprocess +import urllib from xmlrpc.client import ( Fault, Transport, @@ -104,7 +102,7 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"): r = json.loads(v.decode("utf-8")) except Exception as ex: traceback.print_exc() - raise ValueError("RPC server error " + str(ex) + ", method: " + method) + raise ValueError(f"RPC server error: {ex}, method: {method}") if "error" in r and r["error"] is not None: raise ValueError("RPC error " + str(r["error"])) @@ -120,36 +118,7 @@ def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"): return Jsonrpc(url) except Exception as ex: traceback.print_exc() - raise ValueError("RPC error " + str(ex)) - - -def callrpc_cli(bindir, datadir, chain, cmd, cli_bin="particl-cli", wallet=None): - cli_bin = os.path.join(bindir, cli_bin) - - args = [ - cli_bin, - ] - if chain != "mainnet": - args.append("-" + chain) - args.append("-datadir=" + datadir) - if wallet is not None: - args.append("-rpcwallet=" + wallet) - args += shlex.split(cmd) - - p = subprocess.Popen( - args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - out = p.communicate() - - if len(out[1]) > 0: - raise ValueError("RPC error " + str(out[1])) - - r = out[0].decode("utf-8").strip() - try: - r = json.loads(r) - except Exception: - pass - return r + raise ValueError(f"RPC error: {ex}") def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"): diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 1fe2483..5bf2710 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -6,10 +6,12 @@ # Distributed under the MIT software license, see the accompanying # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. -import os import json -import signal import logging +import os +import shlex +import signal +import subprocess from urllib.request import urlopen from .util import read_json_api @@ -133,11 +135,12 @@ def checkForks(ro): def stopDaemons(daemons): for d in daemons: - logging.info("Interrupting %d", d.handle.pid) + logging.info(f"Interrupting {d.handle.pid}") + signal_type = signal.SIGTERM if os.name == "nt" else signal.SIGINT try: - d.handle.send_signal(signal.SIGINT) + d.handle.send_signal(signal_type) except Exception as e: - logging.info("Interrupting %d, error %s", d.handle.pid, str(e)) + logging.info(f"Interrupting {d.handle.pid}, error: {e}") for d in daemons: try: d.handle.wait(timeout=20) @@ -145,7 +148,7 @@ def stopDaemons(daemons): if fp: fp.close() except Exception as e: - logging.info("Closing %d, error %s", d.handle.pid, str(e)) + logging.info(f"Closing {d.handle.pid}, error: {e}") def wait_for_bid( @@ -494,3 +497,38 @@ def compare_bid_states_unordered(states, expect_states, ignore_states=[]) -> boo logging.info("Have states: {}".format(json.dumps(states, indent=4))) raise e return True + + +def callrpc_cli( + bindir, + datadir, + chain, + cmd, + cli_bin="particl-cli" + (".exe" if os.name == "nt" else ""), + wallet=None, +): + cli_bin = os.path.join(bindir, cli_bin) + args = [ + cli_bin, + ] + if chain != "mainnet": + args.append("-" + chain) + args.append("-datadir=" + datadir) + if wallet is not None: + args.append("-rpcwallet=" + wallet) + args += shlex.split(cmd) + + p = subprocess.Popen( + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + out = p.communicate() + + if len(out[1]) > 0: + raise ValueError(f"RPC error: {out[1]}") + + r = out[0].decode("utf-8").strip() + try: + r = json.loads(r) + except Exception: + pass + return r diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py index 583901d..3c847a1 100644 --- a/tests/basicswap/common_xmr.py +++ b/tests/basicswap/common_xmr.py @@ -557,12 +557,12 @@ def prepare_nodes( ): bins_path = os.path.join(TEST_PATH, "bin") for i in range(num_nodes): - logging.info("Preparing node: %d.", i) - client_path = os.path.join(TEST_PATH, "client{}".format(i)) + logging.info(f"Preparing node: {i}.") + client_path = os.path.join(TEST_PATH, f"client{i}") try: shutil.rmtree(client_path) except Exception as ex: - logging.warning("setUpClass %s", str(ex)) + logging.warning(f"setUpClass {ex}") run_prepare( i, diff --git a/tests/basicswap/extended/test_dash.py b/tests/basicswap/extended/test_dash.py index cb90523..1ba2f58 100644 --- a/tests/basicswap/extended/test_dash.py +++ b/tests/basicswap/extended/test_dash.py @@ -40,9 +40,6 @@ from basicswap.basicswap_util import ( from basicswap.util.address import ( toWIF, ) -from basicswap.rpc import ( - callrpc_cli, -) from basicswap.contrib.key import ( ECKey, ) @@ -53,6 +50,7 @@ from tests.basicswap.util import ( read_json_api, ) from tests.basicswap.common import ( + callrpc_cli, checkForks, stopDaemons, wait_for_offer, diff --git a/tests/basicswap/extended/test_firo.py b/tests/basicswap/extended/test_firo.py index b402d5a..236d5e2 100644 --- a/tests/basicswap/extended/test_firo.py +++ b/tests/basicswap/extended/test_firo.py @@ -25,13 +25,11 @@ from basicswap.util import ( make_int, format_amount, ) -from basicswap.rpc import ( - callrpc_cli, -) from tests.basicswap.util import ( read_json_api, ) from tests.basicswap.common import ( + callrpc_cli, stopDaemons, wait_for_bid, make_rpc_func, diff --git a/tests/basicswap/extended/test_network.py b/tests/basicswap/extended/test_network.py index 15df05d..d1a6334 100644 --- a/tests/basicswap/extended/test_network.py +++ b/tests/basicswap/extended/test_network.py @@ -32,7 +32,6 @@ from basicswap.util.address import ( ) from basicswap.rpc import ( callrpc, - callrpc_cli, ) from basicswap.contrib.key import ( ECKey, @@ -44,19 +43,20 @@ from tests.basicswap.util import ( read_json_api, ) from tests.basicswap.common import ( - prepareDataDir, - make_rpc_func, - checkForks, - stopDaemons, - delay_for, - TEST_HTTP_HOST, - TEST_HTTP_PORT, BASE_P2P_PORT, BASE_RPC_PORT, BASE_ZMQ_PORT, BTC_BASE_PORT, BTC_BASE_RPC_PORT, + callrpc_cli, + checkForks, + delay_for, + make_rpc_func, PREFIX_SECRET_KEY_REGTEST, + prepareDataDir, + stopDaemons, + TEST_HTTP_HOST, + TEST_HTTP_PORT, waitForRPC, ) diff --git a/tests/basicswap/extended/test_pivx.py b/tests/basicswap/extended/test_pivx.py index d71e454..e12b918 100644 --- a/tests/basicswap/extended/test_pivx.py +++ b/tests/basicswap/extended/test_pivx.py @@ -40,9 +40,6 @@ from basicswap.basicswap_util import ( from basicswap.util.address import ( toWIF, ) -from basicswap.rpc import ( - callrpc_cli, -) from basicswap.contrib.key import ( ECKey, ) @@ -53,6 +50,7 @@ from tests.basicswap.util import ( read_json_api, ) from tests.basicswap.common import ( + callrpc_cli, checkForks, stopDaemons, wait_for_bid, diff --git a/tests/basicswap/extended/test_wallet_encryption.py b/tests/basicswap/extended/test_wallet_encryption.py new file mode 100644 index 0000000..6f0c7dd --- /dev/null +++ b/tests/basicswap/extended/test_wallet_encryption.py @@ -0,0 +1,1278 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2025 The Basicswap developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import json +import logging +import mmap +import multiprocessing +import os +import shlex +import shutil +import sqlite3 +import subprocess +import sys +import threading +import unittest + + +from unittest.mock import patch +from basicswap.util.ecc import ( + i2b, + getSecretInt, +) +from basicswap.rpc import escape_rpcauth, make_rpc_func +from basicswap.interface.dcr.rpc import make_rpc_func as make_dcr_rpc_func +from tests.basicswap.util import ( + read_json_api, + waitForServer, +) +from basicswap.contrib.rpcauth import generate_salt, password_to_hmac +from basicswap.util.address import ( + b58encode, + toWIF, +) +from basicswap.util.crypto import ( + sha256, +) +from basicswap.util.extkey import ExtKeyPair +from basicswap.contrib.test_framework.descriptors import descsum_create + +bin_path = os.path.expanduser(os.getenv("TEST_BIN_PATH", "")) +test_base_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_basicswap")) + +delay_event = threading.Event() +logger = logging.getLogger() +logger.level = logging.DEBUG +logger.addHandler(logging.StreamHandler(sys.stdout)) + + +def start_prepare(args, datadir=None, env_pairs=[]): + for pair in env_pairs: + os.environ[pair[0]] = pair[1] + print(pair[0], os.environ[pair[0]]) + if datadir: + sys.stdout = open(os.path.join(datadir, "prepare.stdout"), "w") + sys.stderr = open(os.path.join(datadir, "prepare.stderr"), "w") + import basicswap.bin.prepare as prepareSystemThread + + with patch.object(sys, "argv", args): + prepareSystemThread.main() + del prepareSystemThread + + +def start_run(args, datadir=None, env_pairs=[]): + for pair in env_pairs: + os.environ[pair[0]] = pair[1] + print(pair[0], os.environ[pair[0]]) + if datadir: + sys.stdout = open(os.path.join(datadir, "run.stdout"), "w") + sys.stderr = open(os.path.join(datadir, "run.stderr"), "w") + import basicswap.bin.run as runSystemThread + + with patch.object(sys, "argv", args): + runSystemThread.main() + del runSystemThread + + +def callcoincli(binpath, datadir, params, wallet=None, timeout=None): + args = [binpath, "-regtest", "-datadir=" + datadir] + if wallet: + args.append("-rpcwallet=" + wallet) + args += shlex.split(params) + p = subprocess.Popen( + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + out = p.communicate(timeout=timeout) + if len(out[1]) > 0: + raise ValueError("CLI error " + str(out[1])) + return out[0].decode("utf-8").strip() + + +def encode_secret_extkey(prefix, ek_data: bytes) -> str: + assert len(ek_data) == 74 + data: bytes = prefix.to_bytes(4, "big") + ek_data + checksum = sha256(sha256(data)) + return b58encode(data + checksum[0:4]) + + +test_seed: bytes = bytes.fromhex( + "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" +) +test_seedid = "3da5c0af91879e8ce97d9a843874601c08688078" +wif_prefix: int = 239 +test_wif: str = toWIF(wif_prefix, test_seed) + + +class Test(unittest.TestCase): + + test_coins = [ + "particl", + "bitcoin", + "litecoin", + "decred", + "namecoin", + "monero", + "wownero", + "pivx", + "dash", + "firo", + "bitcoincash", + "dogecoin", + ] + + def test_coins_list(self): + test_path = os.path.join(test_base_path, "coins_list") + if os.path.exists(test_path): + shutil.rmtree(test_path) + os.makedirs(test_path) + testargs = ( + "basicswap-prepare", + "-help", + ) + process = multiprocessing.Process( + target=start_prepare, args=(testargs, test_path) + ) + process.start() + process.join() + + with open(os.path.join(test_path, "prepare.stdout"), "r") as fp: + output = fp.read() + + known_coins_line = None + for line in output.split("\n"): + if line.startswith("Known coins: "): + known_coins_line = line[13:] + assert known_coins_line + known_coins = known_coins_line.split(", ") + + for known_coin in known_coins: + if known_coin not in self.test_coins: + raise ValueError(f"Not testing: {known_coin}") + for test_coin in self.test_coins: + if test_coin not in known_coins: + raise ValueError(f"Unknown coin: {test_coin}") + + def test_with_encrypt(self): + test_path = os.path.join(test_base_path, "with_encrypt") + if os.path.exists(test_path): + shutil.rmtree(test_path) + os.makedirs(test_path) + if bin_path != "": + os.symlink(bin_path, os.path.join(test_path, "bin")) + + env_vars = [ + ("WALLET_ENCRYPTION_PWD", "test.123"), + ] + testargs = [ + "basicswap-prepare", + "-regtest=1", + "-datadir=" + test_path, + "-withcoin=" + ",".join(self.test_coins), + ] + process = multiprocessing.Process( + target=start_prepare, args=(testargs, test_path, env_vars) + ) + process.start() + process.join() + assert process.exitcode == 0 + + with open(os.path.join(test_path, "prepare.stdout"), "r") as fp: + output = fp.read() + + note_lines = [] + warning_lines = [] + for line in output.split("\n"): + print("line", line) + if line.startswith("NOTE -"): + note_lines.append(line) + if line.startswith("WARNING -"): + warning_lines.append(line) + + assert len(warning_lines) == 1 + assert any( + "WARNING - dcrwallet requires the password to be entered at the first startup when encrypted." + in x + for x in warning_lines + ) + + assert len(note_lines) == 2 + assert any("Unable to initialise wallet for PIVX." in x for x in note_lines) + assert any( + "Unable to initialise wallet for Bitcoin Cash." in x for x in note_lines + ) + + dcr_rpcport = None + dcr_rpcuser = None + dcr_rpcpass = None + bch_rpcport = None + pivx_rpcport = None + # Make (regtest) ports unique + settings_path = os.path.join(test_path, "basicswap.json") + with open(settings_path) as fs: + settings = json.load(fs) + settings["chainclients"]["dogecoin"]["port"] = 12444 + dcr_rpcuser = settings["chainclients"]["decred"]["rpcuser"] + dcr_rpcpass = settings["chainclients"]["decred"]["rpcpassword"] + dcr_rpcport = settings["chainclients"]["decred"]["rpcport"] + bch_rpcport = settings["chainclients"]["bitcoincash"]["rpcport"] + pivx_rpcport = settings["chainclients"]["pivx"]["rpcport"] + with open(settings_path, "w") as fp: + json.dump(settings, fp, indent=4) + + dcr_conf_path = os.path.join(test_path, "decred", "dcrd.conf") + with open(dcr_conf_path, "a") as fp: + fp.write("miningaddr=SsjkQJHak5pRVUdUzqyFHKnojCVRZMU24w6\n") + + testargs = [ + "basicswap-run", + "-regtest=1", + "-datadir=" + test_path, + "--startonlycoin=decred", + ] + process = multiprocessing.Process( + target=start_run, args=(testargs, test_path, env_vars) + ) + process.start() + try: + auth = f"{dcr_rpcuser}:{dcr_rpcpass}" + dcr_rpc = make_dcr_rpc_func(dcr_rpcport, auth) + for i in range(10): + try: + rv = dcr_rpc("generate", [110]) + break + except Exception as e: # noqa: F841 + delay_event.wait(1.0) + + finally: + process.terminate() + process.join() + assert process.exitcode == 0 + + testargs = ["basicswap-run", "-regtest=1", "-datadir=" + test_path] + process = multiprocessing.Process(target=start_run, args=(testargs, test_path)) + process.start() + try: + waitForServer(delay_event, 12700, wait_for=40) + logging.info("Unlocking") + rv = read_json_api(12700, "unlock", {"password": "test.123"}) + assert "success" in rv + + for coin in self.test_coins: + if coin == "particl": + continue + rv = read_json_api(12700, "getcoinseed", {"coin": coin}) + if coin in ("monero", "wownero"): + assert rv["address"] == rv["expected_address"] + elif coin in ("bitcoincash", "pivx"): + assert rv["seed_id"] == rv["expected_seed_id"] + # Reseed required + assert rv["seed_id"] != rv["current_seed_id"] + else: + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + authcookiepath = os.path.join( + test_path, "bitcoincash", "regtest", ".cookie" + ) + with open(authcookiepath, "rb") as fp: + bch_rpcauth = escape_rpcauth(fp.read().decode("utf-8")) + + bch_rpc = make_rpc_func(bch_rpcport, bch_rpcauth) + + bch_addr = bch_rpc("getnewaddress") + rv = bch_rpc("generatetoaddress", [1, bch_addr]) + rv = read_json_api(12700, "wallets/bch/reseed") + assert rv["reseeded"] is True + + authcookiepath = os.path.join(test_path, "pivx", "regtest", ".cookie") + with open(authcookiepath, "rb") as fp: + pivx_rpcauth = escape_rpcauth(fp.read().decode("utf-8")) + + pivx_rpc = make_rpc_func(pivx_rpcport, pivx_rpcauth) + + pivx_addr = pivx_rpc("getnewaddress") + rv = pivx_rpc("generatetoaddress", [1, pivx_addr]) + rv = read_json_api(12700, "wallets/pivx/reseed") + assert rv["reseeded"] is True + + for coin in self.test_coins: + if coin == "particl": + continue + rv = read_json_api(12700, "getcoinseed", {"coin": coin}) + if coin in ("monero", "wownero"): + assert rv["address"] == rv["expected_address"] + else: + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + ltc_cli_path = os.path.join(test_path, "bin", "litecoin", "litecoin-cli") + ltc_datadir = os.path.join(test_path, "litecoin") + rv = json.loads( + callcoincli( + ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="wallet.dat" + ) + ) + assert "unlocked_until" in rv + rv = json.loads( + callcoincli(ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="mweb") + ) + assert "unlocked_until" in rv + finally: + process.terminate() + process.join() + assert process.exitcode == 0 + + def test_with_encrypt_addcoin(self): + test_path = os.path.join(test_base_path, "encrypt_addcoin") + if os.path.exists(test_path): + shutil.rmtree(test_path) + os.makedirs(test_path) + if bin_path != "": + os.symlink(bin_path, os.path.join(test_path, "bin")) + + env_vars = [ + ("WALLET_ENCRYPTION_PWD", "test.123"), + ] + testargs = [ + "basicswap-prepare", + "-regtest=1", + "-datadir=" + test_path, + ] + process = multiprocessing.Process( + target=start_prepare, args=(testargs, test_path, env_vars) + ) + process.start() + process.join() + assert process.exitcode == 0 + + for coin in self.test_coins: + if coin == "particl": + continue + testargs = [ + "basicswap-prepare", + "-regtest=1", + "-datadir=" + test_path, + "-addcoin=" + coin, + ] + process = multiprocessing.Process( + target=start_prepare, args=(testargs, test_path, env_vars) + ) + process.start() + process.join() + assert process.exitcode == 0 + + dcr_rpcport = None + dcr_rpcuser = None + dcr_rpcpass = None + bch_rpcport = None + pivx_rpcport = None + # Make (regtest) ports unique + settings_path = os.path.join(test_path, "basicswap.json") + with open(settings_path) as fs: + settings = json.load(fs) + settings["chainclients"]["dogecoin"]["port"] = 12444 + dcr_rpcuser = settings["chainclients"]["decred"]["rpcuser"] + dcr_rpcpass = settings["chainclients"]["decred"]["rpcpassword"] + dcr_rpcport = settings["chainclients"]["decred"]["rpcport"] + bch_rpcport = settings["chainclients"]["bitcoincash"]["rpcport"] + pivx_rpcport = settings["chainclients"]["pivx"]["rpcport"] + with open(settings_path, "w") as fp: + json.dump(settings, fp, indent=4) + + dcr_conf_path = os.path.join(test_path, "decred", "dcrd.conf") + with open(dcr_conf_path, "a") as fp: + fp.write("miningaddr=SsjkQJHak5pRVUdUzqyFHKnojCVRZMU24w6\n") + + testargs = [ + "basicswap-run", + "-regtest=1", + "-datadir=" + test_path, + "--startonlycoin=decred", + ] + process = multiprocessing.Process( + target=start_run, args=(testargs, test_path, env_vars) + ) + process.start() + try: + auth = f"{dcr_rpcuser}:{dcr_rpcpass}" + dcr_rpc = make_dcr_rpc_func(dcr_rpcport, auth) + for i in range(10): + try: + rv = dcr_rpc("generate", [110]) + break + except Exception as e: # noqa: F841 + delay_event.wait(1.0) + finally: + process.terminate() + process.join() + assert process.exitcode == 0 + + testargs = ["basicswap-run", "-regtest=1", "-datadir=" + test_path] + process = multiprocessing.Process(target=start_run, args=(testargs, test_path)) + process.start() + try: + waitForServer(delay_event, 12700, wait_for=40) + logging.info("Unlocking") + rv = read_json_api(12700, "unlock", {"password": "test.123"}) + assert "success" in rv + + for coin in self.test_coins: + if coin == "particl": + continue + rv = read_json_api(12700, "getcoinseed", {"coin": coin}) + if coin in ("monero", "wownero"): + assert rv["address"] == rv["expected_address"] + elif coin in ("bitcoincash", "pivx"): + assert rv["seed_id"] == rv["expected_seed_id"] + # Reseed required + assert rv["seed_id"] != rv["current_seed_id"] + else: + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + authcookiepath = os.path.join( + test_path, "bitcoincash", "regtest", ".cookie" + ) + with open(authcookiepath, "rb") as fp: + bch_rpcauth = escape_rpcauth(fp.read().decode("utf-8")) + + bch_rpc = make_rpc_func(bch_rpcport, bch_rpcauth) + + logging.info("Reseeding BCH") + bch_addr = bch_rpc("getnewaddress") + rv = bch_rpc("generatetoaddress", [1, bch_addr]) + rv = read_json_api(12700, "wallets/bch/reseed") + assert rv["reseeded"] is True + + logging.info("Reseeding PIVX") + authcookiepath = os.path.join(test_path, "pivx", "regtest", ".cookie") + with open(authcookiepath, "rb") as fp: + pivx_rpcauth = escape_rpcauth(fp.read().decode("utf-8")) + + pivx_rpc = make_rpc_func(pivx_rpcport, pivx_rpcauth) + + pivx_addr = pivx_rpc("getnewaddress") + rv = pivx_rpc("generatetoaddress", [1, pivx_addr]) + rv = read_json_api(12700, "wallets/pivx/reseed") + assert rv["reseeded"] is True + + for coin in self.test_coins: + if coin == "particl": + continue + rv = read_json_api(12700, "getcoinseed", {"coin": coin}) + if coin in ("monero", "wownero"): + assert rv["address"] == rv["expected_address"] + else: + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + ltc_cli_path = os.path.join(test_path, "bin", "litecoin", "litecoin-cli") + ltc_datadir = os.path.join(test_path, "litecoin") + rv = json.loads( + callcoincli( + ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="wallet.dat" + ) + ) + assert "unlocked_until" in rv + rv = json.loads( + callcoincli(ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="mweb") + ) + assert "unlocked_until" in rv + finally: + process.terminate() + process.join() + assert process.exitcode == 0 + + # Check that BSX starts up again + testargs = ["basicswap-run", "-regtest=1", "-datadir=" + test_path] + process = multiprocessing.Process(target=start_run, args=(testargs, test_path)) + process.start() + try: + waitForServer(delay_event, 12700, wait_for=40) + + finally: + process.terminate() + process.join() + assert process.exitcode == 0 + + def test_encrypt_after(self): + test_path = os.path.join(test_base_path, "encrypt_after") + if os.path.exists(test_path): + shutil.rmtree(test_path) + os.makedirs(test_path) + if bin_path != "": + os.symlink(bin_path, os.path.join(test_path, "bin")) + + testargs = [ + "basicswap-prepare", + "-regtest=1", + "-datadir=" + test_path, + "-withcoin=" + ",".join(self.test_coins), + ] + process = multiprocessing.Process( + target=start_prepare, args=(testargs, test_path) + ) + process.start() + process.join() + assert process.exitcode == 0 + + dcr_rpcport = None + dcr_rpcuser = None + dcr_rpcpass = None + bch_rpcport = None + pivx_rpcport = None + # Make (regtest) ports unique + settings_path = os.path.join(test_path, "basicswap.json") + with open(settings_path) as fs: + settings = json.load(fs) + settings["chainclients"]["dogecoin"]["port"] = 12444 + dcr_rpcuser = settings["chainclients"]["decred"]["rpcuser"] + dcr_rpcpass = settings["chainclients"]["decred"]["rpcpassword"] + dcr_rpcport = settings["chainclients"]["decred"]["rpcport"] + bch_rpcport = settings["chainclients"]["bitcoincash"]["rpcport"] + pivx_rpcport = settings["chainclients"]["pivx"]["rpcport"] + + with open(settings_path, "w") as fp: + json.dump(settings, fp, indent=4) + + dcr_conf_path = os.path.join(test_path, "decred", "dcrd.conf") + with open(dcr_conf_path, "a") as fp: + fp.write("miningaddr=SsjkQJHak5pRVUdUzqyFHKnojCVRZMU24w6\n") + + testargs = ["basicswap-run", "-regtest=1", "-datadir=" + test_path] + process = multiprocessing.Process(target=start_run, args=(testargs, test_path)) + process.start() + try: + waitForServer(delay_event, 12700, wait_for=40) + + for coin in self.test_coins: + if coin == "particl": + continue + rv = read_json_api(12700, "getcoinseed", {"coin": coin}) + if coin in ("monero", "wownero"): + assert rv["address"] == rv["expected_address"] + elif coin in ("bitcoincash", "pivx"): + assert rv["seed_id"] == rv["expected_seed_id"] + # Reseed required + assert rv["seed_id"] != rv["current_seed_id"] + else: + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + authcookiepath = os.path.join( + test_path, "bitcoincash", "regtest", ".cookie" + ) + with open(authcookiepath, "rb") as fp: + bch_rpcauth = escape_rpcauth(fp.read().decode("utf-8")) + + bch_rpc = make_rpc_func(bch_rpcport, bch_rpcauth) + + bch_addr = bch_rpc("getnewaddress") + rv = bch_rpc("generatetoaddress", [1, bch_addr]) + rv = read_json_api(12700, "wallets/bch/reseed") + assert rv["reseeded"] is True + + authcookiepath = os.path.join(test_path, "pivx", "regtest", ".cookie") + with open(authcookiepath, "rb") as fp: + pivx_rpcauth = escape_rpcauth(fp.read().decode("utf-8")) + + pivx_rpc = make_rpc_func(pivx_rpcport, pivx_rpcauth) + + pivx_addr = pivx_rpc("getnewaddress") + rv = pivx_rpc("generatetoaddress", [1, pivx_addr]) + rv = read_json_api(12700, "wallets/pivx/reseed") + assert rv["reseeded"] is True + + for coin in self.test_coins: + if coin == "particl": + continue + rv = read_json_api(12700, "getcoinseed", {"coin": coin}) + if coin in ("monero", "wownero"): + assert rv["address"] == rv["expected_address"] + else: + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + ltc_cli_path = os.path.join(test_path, "bin", "litecoin", "litecoin-cli") + ltc_datadir = os.path.join(test_path, "litecoin") + rv = json.loads( + callcoincli(ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="mweb") + ) + ltc_mweb_seed_before: str = rv["hdseedid"] + assert "unlocked_until" not in rv + + # Get Decred out of IBD, else first start after encryption will lock up with "Since this is your first time running we need to sync accounts." + auth = f"{dcr_rpcuser}:{dcr_rpcpass}" + dcr_rpc = make_dcr_rpc_func(dcr_rpcport, auth) + for i in range(10): + try: + rv = dcr_rpc("generate", [110]) + break + except Exception as e: # noqa: F841 + delay_event.wait(1.0) + + logging.info("setpassword (encrypt wallets)") + rv = read_json_api( + 12700, "setpassword", {"oldpassword": "", "newpassword": "test.123"} + ) + assert "success" in rv + + logging.info("Unlocking") + rv = read_json_api(12700, "unlock", {"password": "test.123"}) + assert "success" in rv + + for coin in self.test_coins: + logging.info(f"Coin: {coin}") + if coin == "particl": + continue + if coin == "firo": + # firo core shuts down after encryptwallet + continue + rv = read_json_api(12700, "getcoinseed", {"coin": coin}) + if coin in ("monero", "wownero"): + assert rv["address"] == rv["expected_address"] + elif coin in ("pivx"): + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] != rv["current_seed_id"] + else: + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + # pivx seed has changed + rv = read_json_api(12700, "wallets/pivx") + assert rv["expected_seed"] is False + + logging.info("Try to reseed pivx (and fail).") + rv = read_json_api(12700, "wallets/pivx/reseed") + assert "Already have this key" in rv["error"] + + logging.info("Check both LTC wallets are encrypted and mweb seeds match.") + rv = json.loads( + callcoincli( + ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="wallet.dat" + ) + ) + assert "unlocked_until" in rv + rv = json.loads( + callcoincli(ltc_cli_path, ltc_datadir, "getwalletinfo", wallet="mweb") + ) + assert "unlocked_until" in rv + assert ltc_mweb_seed_before == rv["hdseedid"] + + finally: + process.terminate() + process.join() + assert process.exitcode == 0 + + logging.info("Starting BSX to check Firo") + testargs = ["basicswap-run", "-regtest=1", "-datadir=" + test_path] + process = multiprocessing.Process(target=start_run, args=(testargs, test_path)) + process.start() + try: + waitForServer(delay_event, 12700, wait_for=40) + logging.info("Unlocking") + rv = read_json_api(12700, "unlock", {"password": "test.123"}) + assert "success" in rv + + rv = read_json_api(12700, "getcoinseed", {"coin": "firo"}) + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + rv = read_json_api(12700, "getcoinseed", {"coin": "dcr"}) + assert rv["seed_id"] == rv["expected_seed_id"] + assert rv["seed_id"] == rv["current_seed_id"] + + finally: + process.terminate() + process.join() + assert process.exitcode == 0 + + def write_btc_conf(self, datadir): + conf_path = os.path.join(datadir, "bitcoin.conf") + with open(conf_path, "w") as fp: + fp.write("regtest=1\n") + fp.write("[regtest]\n") + fp.write("printtoconsole=0\n") + fp.write("server=1\n") + fp.write("discover=0\n") + fp.write("listenonion=0\n") + fp.write("bind=127.0.0.1\n") + fp.write("debug=1\n") + fp.write("debugexclude=libevent\n") + fp.write("deprecatedrpc=create_bdb\n") + fp.write("rpcport=12223\n") + salt = generate_salt(16) + fp.write( + "rpcauth={}:{}${}\n".format( + "test", + salt, + password_to_hmac(salt, "test_pass"), + ) + ) + + def test_btc_wallets_with_module(self): + import berkeleydb + + if not os.path.exists(bin_path): + raise ValueError("TEST_BIN_PATH not set.") + test_path = os.path.join(test_base_path, "test_btc_wallets_with_module") + if os.path.exists(test_path): + shutil.rmtree(test_path) + os.makedirs(test_path) + self.write_btc_conf(test_path) + daemon_path = os.path.join(bin_path, "bitcoin", "bitcoind") + args = [ + daemon_path, + "-datadir=" + test_path, + ] + bitcoind_process = subprocess.Popen(args) + try: + rpc = make_rpc_func(12223, "test:test_pass") + for i in range(20): + try: + rv = rpc("listwallets") + break + except Exception as e: # noqa: F841 + delay_event.wait(1.0) + + # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors + rpc( + "createwallet", + ["bdb_wallet", False, True, "", False, False], + ) + + rpc("sethdseed", [True, test_wif], wallet_override="bdb_wallet") + rv = rpc("getwalletinfo", wallet_override="bdb_wallet") + assert rv["hdseedid"] == test_seedid + + new_addr = rpc("getnewaddress", wallet_override="bdb_wallet") + logging.info(f"getnewaddress {new_addr}") + rv = rpc( + "getaddressinfo", + [ + new_addr, + ], + wallet_override="bdb_wallet", + ) + logging.info(f"getaddressinfo before encrypt {rv}") + assert rv["hdmasterfingerprint"] == "a55b7ea9" + + rpc( + "unloadwallet", + [ + "bdb_wallet", + ], + ) + + walletdir = os.path.join(test_path, "regtest", "wallets", "bdb_wallet") + walletpath = os.path.join(walletdir, "wallet.dat") + + db = berkeleydb.db.DB() + db.open( + walletpath, + "main", + berkeleydb.db.DB_BTREE, + berkeleydb.db.DB_THREAD | berkeleydb.db.DB_CREATE, + ) + prev_hdchain_key = None + prev_hdchain_value = None + for k, v in db.items(): + if b"hdchain" in k: + prev_hdchain_key = k + prev_hdchain_value = v + db.close() + + rpc( + "loadwallet", + [ + "bdb_wallet", + ], + ) + + rv = rpc( + "encryptwallet", + [ + "test.123", + ], + wallet_override="bdb_wallet", + ) + logging.info(f"encryptwallet {rv}") + rv = rpc("getwalletinfo", wallet_override="bdb_wallet") + logging.info(f"getwalletinfo {rv}") + assert rv["hdseedid"] != test_seedid + + rpc( + "unloadwallet", + [ + "bdb_wallet", + ], + ) + + bkp_path = os.path.join(walletdir, "wallet.dat" + ".bkp") + for i in range(1000): + if os.path.exists(bkp_path): + bkp_path = os.path.join(walletdir, "wallet.dat" + f".bkp{i}") + + assert os.path.exists(bkp_path) is False + if os.path.isfile(walletpath): + shutil.copy(walletpath, bkp_path) + else: + shutil.copytree(walletpath, bkp_path) + + # Replace hdchain with previous value + db = berkeleydb.db.DB() + db.open( + walletpath, + "main", + berkeleydb.db.DB_BTREE, + berkeleydb.db.DB_THREAD | berkeleydb.db.DB_CREATE, + ) + db[prev_hdchain_key] = prev_hdchain_value + db.close() + + rpc( + "loadwallet", + [ + "bdb_wallet", + ], + ) + + rv = rpc("getwalletinfo", wallet_override="bdb_wallet") + logging.info(f"getwalletinfo {rv}") + assert rv["hdseedid"] == test_seedid + + # Picks from wrong keypool + new_addr = rpc("getnewaddress", wallet_override="bdb_wallet") + rv = rpc( + "getaddressinfo", + [ + new_addr, + ], + wallet_override="bdb_wallet", + ) + assert rv["hdmasterfingerprint"] != "a55b7ea9" + + rpc("walletpassphrase", ["test.123", 1000], wallet_override="bdb_wallet") + rpc("newkeypool", wallet_override="bdb_wallet") + + new_addr = rpc("getnewaddress", wallet_override="bdb_wallet") + rv = rpc( + "getaddressinfo", + [ + new_addr, + ], + wallet_override="bdb_wallet", + ) + assert rv["hdmasterfingerprint"] == "a55b7ea9" + + finally: + rpc("stop") + bitcoind_process.wait(timeout=30) + + def test_btc_wallets_without_module(self): + if not os.path.exists(bin_path): + raise ValueError("TEST_BIN_PATH not set.") + test_path = os.path.join(test_base_path, "test_btc_wallets_without_module") + if os.path.exists(test_path): + shutil.rmtree(test_path) + os.makedirs(test_path) + self.write_btc_conf(test_path) + daemon_path = os.path.join(bin_path, "bitcoin", "bitcoind") + args = [ + daemon_path, + "-datadir=" + test_path, + ] + bitcoind_process = subprocess.Popen(args) + try: + rpc = make_rpc_func(12223, "test:test_pass") + for i in range(20): + try: + rv = rpc("listwallets") + break + except Exception as e: # noqa: F841 + delay_event.wait(1.0) + + rv = rpc( + "createwallet", + ["bdb_wallet2", False, True, "", False, False], + ) + logging.info(f"createwallet {rv}") + + rpc("sethdseed", [True, test_wif], wallet_override="bdb_wallet2") + + new_addr = rpc("getnewaddress", wallet_override="bdb_wallet2") + logging.info(f"getnewaddress {new_addr}") + rv = rpc( + "getaddressinfo", + [ + new_addr, + ], + wallet_override="bdb_wallet2", + ) + logging.info(f"getaddressinfo before encrypt {rv}") + assert rv["hdmasterfingerprint"] == "a55b7ea9" + + rv = rpc("getwalletinfo", wallet_override="bdb_wallet2") + logging.info(f"getwalletinfo {rv}") + assert rv["hdseedid"] == test_seedid + + seedid_bytes = bytes.fromhex(rv["hdseedid"])[::-1] + # keypoolsize and keypoolsize_hd_internal are how many pre-generated keys remain unused. + orig_hdchain_bytes_predicted = ( + int(2).to_bytes(4, "little") + + int(1000).to_bytes(4, "little") + + seedid_bytes + + int(1000).to_bytes(4, "little") + ) + logging.info( + f"orig_hdchain_bytes_predicted {orig_hdchain_bytes_predicted.hex()}" + ) + + rv = rpc( + "unloadwallet", + [ + "bdb_wallet2", + ], + ) + logging.info(f"Looking for hdchain for {seedid_bytes.hex()}") + walletdir = os.path.join(test_path, "regtest", "wallets", "bdb_wallet2") + walletpath = os.path.join(walletdir, "wallet.dat") + found_hdchain = False + max_key_count = 4000000 # arbitrary + with open(walletpath, "rb") as fp: + with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as mm: + pos = mm.find(seedid_bytes) + while pos != -1: + mm.seek(pos - 8) + hdchain_bytes = mm.read(12 + 20) + version = int.from_bytes(hdchain_bytes[:4], "little") + if version == 2: + external_counter = int.from_bytes( + hdchain_bytes[4:8], "little" + ) + internal_counter = int.from_bytes( + hdchain_bytes[-4:], "little" + ) + if ( + external_counter > 0 + and external_counter <= max_key_count + and internal_counter > 0 + and internal_counter <= max_key_count + ): + orig_hdchain_bytes = hdchain_bytes + found_hdchain = True + break + pos = mm.find(seedid_bytes, pos + 1) + logging.info(f"orig_hdchain_bytes {orig_hdchain_bytes.hex()}") + assert found_hdchain + + rpc( + "loadwallet", + [ + "bdb_wallet2", + ], + ) + + rv = rpc( + "encryptwallet", + [ + "test.123", + ], + wallet_override="bdb_wallet2", + ) + logging.info(f"encryptwallet {rv}") + rv = rpc("getwalletinfo", wallet_override="bdb_wallet2") + logging.info(f"getwalletinfo {rv}") + assert rv["hdseedid"] != test_seedid + + new_hdchain_bytes = ( + int(2).to_bytes(4, "little") + + int(rv["keypoolsize"]).to_bytes(4, "little") + + bytes.fromhex(rv["hdseedid"])[::-1] + + int(rv["keypoolsize_hd_internal"]).to_bytes(4, "little") + ) + + rpc( + "unloadwallet", + [ + "bdb_wallet2", + ], + ) + + with open(walletpath, "r+b") as fp: + with mmap.mmap(fp.fileno(), 0) as mm: + offset = mm.find(new_hdchain_bytes) + if offset != -1: + mm.seek(offset) + mm.write(orig_hdchain_bytes) + print(f"Replaced at offset: {offset}") + else: + print("Byte sequence not found.") + + rpc( + "loadwallet", + [ + "bdb_wallet2", + ], + ) + + rv = rpc("getwalletinfo", wallet_override="bdb_wallet2") + logging.info(f"getwalletinfo {rv}") + assert rv["hdseedid"] == test_seedid + + rpc("walletpassphrase", ["test.123", 1000], wallet_override="bdb_wallet2") + rpc("newkeypool", wallet_override="bdb_wallet2") + + new_addr = rpc("getnewaddress", wallet_override="bdb_wallet2") + rv = rpc( + "getaddressinfo", + [ + new_addr, + ], + wallet_override="bdb_wallet2", + ) + assert rv["hdmasterfingerprint"] == "a55b7ea9" + finally: + rpc("stop") + bitcoind_process.wait(timeout=30) + + def test_btc_wallets_descriptors(self): + if not os.path.exists(bin_path): + raise ValueError("TEST_BIN_PATH not set.") + test_path = os.path.join(test_base_path, "test_btc_wallets_descriptors") + if os.path.exists(test_path): + shutil.rmtree(test_path) + os.makedirs(test_path) + self.write_btc_conf(test_path) + daemon_path = os.path.join(bin_path, "bitcoin", "bitcoind") + args = [ + daemon_path, + "-datadir=" + test_path, + ] + bitcoind_process = subprocess.Popen(args) + try: + rpc = make_rpc_func(12223, "test:test_pass") + for i in range(20): + try: + rv = rpc("listwallets") + break + except Exception as e: # noqa: F841 + delay_event.wait(1.0) + + rpc( + "createwallet", + ["descr_wallet", False, True, "", False, True], + ) + + ek = ExtKeyPair() + ek.set_seed(test_seed) + ek_encoded: str = encode_secret_extkey(0x04358394, ek.encode_v()) + + desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)") + desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)") + rv = rpc( + "importdescriptors", + [ + [ + { + "desc": desc_external, + "timestamp": "now", + "active": True, + "range": [0, 10], + "next_index": 0, + }, + { + "desc": desc_internal, + "timestamp": "now", + "active": True, + "internal": True, + }, + ], + ], + wallet_override="descr_wallet", + ) + logging.info(f"importdescriptors {rv}") + addr = rpc( + "getnewaddress", ["test descriptors"], wallet_override="descr_wallet" + ) + addr_info = rpc( + "getaddressinfo", + [ + addr, + ], + wallet_override="descr_wallet", + ) + assert addr_info["hdmasterfingerprint"] == "a55b7ea9" + + descriptors_before = rpc( + "listdescriptors", [], wallet_override="descr_wallet" + ) + logging.info(f"descriptors_before {descriptors_before}") + + rv = rpc("getwalletinfo", wallet_override="descr_wallet") + logging.info(f"getwalletinfo {rv}") + + new_addr = rpc("getnewaddress", wallet_override="descr_wallet") + rv = rpc( + "getaddressinfo", + [ + new_addr, + ], + wallet_override="descr_wallet", + ) + assert rv["hdmasterfingerprint"] == "a55b7ea9" + + rpc( + "unloadwallet", + [ + "descr_wallet", + ], + ) + + walletdir = os.path.join(test_path, "regtest", "wallets", "descr_wallet") + walletpath = os.path.join(walletdir, "wallet.dat") + + orig_active_descriptors = [] + with sqlite3.connect(walletpath) as conn: + c = conn.cursor() + rows = c.execute( + "SELECT * FROM main WHERE key in (:kext, :kint)", + { + "kext": bytes.fromhex("1161637469766565787465726e616c73706b02"), + "kint": bytes.fromhex("11616374697665696e7465726e616c73706b02"), + }, + ) + for row in rows: + k, v = row + orig_active_descriptors.append({"k": k, "v": v}) + + assert len(orig_active_descriptors) == 2 + rpc( + "loadwallet", + [ + "descr_wallet", + ], + ) + + rv = rpc( + "encryptwallet", + [ + "test.123", + ], + wallet_override="descr_wallet", + ) + logging.info(f"encryptwallet {rv}") + + rv = rpc("listdescriptors", [], wallet_override="descr_wallet") + logging.info(f"listdescriptors {rv}") + + # The descriptors don't seem to be replaced + addr = rpc( + "getnewaddress", ["test descriptors"], wallet_override="descr_wallet" + ) + addr_info = rpc( + "getaddressinfo", + [ + addr, + ], + wallet_override="descr_wallet", + ) + assert addr_info["hdmasterfingerprint"] == "a55b7ea9" + + # Simulate, in case it changes + rpc("walletpassphrase", ["test.123", 1000], wallet_override="descr_wallet") + ek = ExtKeyPair() + ek.set_seed(i2b(getSecretInt())) + ek_encoded: str = encode_secret_extkey(0x04358394, ek.encode_v()) + + desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)") + desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)") + rv = rpc( + "importdescriptors", + [ + [ + { + "desc": desc_external, + "timestamp": "now", + "active": True, + "range": [0, 10], + "next_index": 0, + }, + { + "desc": desc_internal, + "timestamp": "now", + "active": True, + "internal": True, + }, + ], + ], + wallet_override="descr_wallet", + ) + logging.info(f"importdescriptors {rv}") + addr = rpc( + "getnewaddress", ["test descriptors"], wallet_override="descr_wallet" + ) + addr_info = rpc( + "getaddressinfo", + [ + addr, + ], + wallet_override="descr_wallet", + ) + assert addr_info["hdmasterfingerprint"] != "a55b7ea9" + + rv = rpc("getwalletinfo", [], wallet_override="descr_wallet") + logging.info(f"getwalletinfo {rv}") + + rpc( + "unloadwallet", + [ + "descr_wallet", + ], + ) + bkp_path = os.path.join(walletdir, "wallet.dat" + ".bkp") + for i in range(1000): + if os.path.exists(bkp_path): + bkp_path = os.path.join(walletdir, "wallet.dat" + f".bkp{i}") + + assert os.path.exists(bkp_path) is False + if os.path.isfile(walletpath): + shutil.copy(walletpath, bkp_path) + else: + shutil.copytree(walletpath, bkp_path) + + with sqlite3.connect(walletpath) as conn: + c = conn.cursor() + c.executemany( + "UPDATE main SET value = :v WHERE key = :k", orig_active_descriptors + ) + conn.commit() + + rpc( + "loadwallet", + [ + "descr_wallet", + ], + ) + addr = rpc("getnewaddress", wallet_override="descr_wallet") + addr_info = rpc( + "getaddressinfo", + [ + addr, + ], + wallet_override="descr_wallet", + ) + logging.info(f"getaddressinfo {addr_info}") + assert addr_info["hdmasterfingerprint"] == "a55b7ea9" + + finally: + rpc("stop") + bitcoind_process.wait(timeout=30) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/basicswap/extended/test_wow.py b/tests/basicswap/extended/test_wow.py index 8a022e0..ca5729a 100644 --- a/tests/basicswap/extended/test_wow.py +++ b/tests/basicswap/extended/test_wow.py @@ -174,7 +174,8 @@ class Test(BaseTest): "create_wallet", {"filename": "testwallet", "language": "English"}, ) - cls.callwownodewallet(cls, i, "open_wallet", {"filename": "testwallet"}) + else: + cls.callwownodewallet(cls, i, "open_wallet", {"filename": "testwallet"}) @classmethod def addPIDInfo(cls, sc, i): diff --git a/tests/basicswap/test_bch_xmr.py b/tests/basicswap/test_bch_xmr.py index be76fe5..4ebfa84 100644 --- a/tests/basicswap/test_bch_xmr.py +++ b/tests/basicswap/test_bch_xmr.py @@ -20,6 +20,7 @@ from basicswap.bin.run import startDaemon from basicswap.util.crypto import sha256 from tests.basicswap.test_btc_xmr import BasicSwapTest from tests.basicswap.common import ( + callrpc_cli, make_rpc_func, prepareDataDir, stopDaemons, @@ -37,9 +38,6 @@ from basicswap.contrib.test_framework.script import ( OP_CHECKSEQUENCEVERIFY, ) from basicswap.interface.bch import BCHInterface -from basicswap.rpc import ( - callrpc_cli, -) from basicswap.util import ensure from .test_xmr import test_delay_event, callnoderpc @@ -134,14 +132,20 @@ class TestBCH(BasicSwapTest): if not line.startswith("findpeers"): fp.write(line) - if os.path.exists(os.path.join(BITCOINCASH_BINDIR, "bitcoin-wallet")): + bch_wallet_bin = "bitcoin-wallet" + (".exe" if os.name == "nt" else "") + if os.path.exists( + os.path.join( + BITCOINCASH_BINDIR, + bch_wallet_bin, + ) + ): try: callrpc_cli( BITCOINCASH_BINDIR, data_dir, "regtest", "-wallet=wallet.dat create", - "bitcoin-wallet", + bch_wallet_bin, ) except Exception as e: logging.warning("bch: bitcoin-wallet create failed") diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index f8b8e41..facaa63 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -1740,6 +1740,74 @@ class BasicSwapTest(TestFunctions): self.callnoderpc("unloadwallet", [new_wallet_name]) self.callnoderpc("unloadwallet", [new_watch_wallet_name]) + def test_014_encrypt_existing_wallet(self): + logging.info( + f"---------- Test {self.test_coin_from.name} encrypt existing wallet" + ) + + ci = self.swap_clients[0].ci(self.test_coin_from) + wallet_name = "encrypt_existing_wallet" + + ci.createWallet(wallet_name) + ci.setActiveWallet(wallet_name) + chain_client_settings = self.swap_clients[0].getChainClientSettings( + self.test_coin_from + ) + try: + chain_client_settings["manage_daemon"] = True + ci.initialiseWallet(ci.getNewRandomKey()) + + original_seed_id = ci.getWalletSeedID() + + addr1 = ci.getNewAddress(True) + addr1_info = ci.rpc_wallet( + "getaddressinfo", + [ + addr1, + ], + ) + + addr_int1 = ci.rpc_wallet("getrawchangeaddress") + addr_int1_info = ci.rpc_wallet( + "getaddressinfo", + [ + addr_int1, + ], + ) + + ci.encryptWallet("test.123") + + after_seed_id = ci.getWalletSeedID() + + addr2 = ci.getNewAddress(True) + addr2_info = ci.rpc_wallet( + "getaddressinfo", + [ + addr2, + ], + ) + + addr_int2 = ci.rpc_wallet("getrawchangeaddress") + addr_int2_info = ci.rpc_wallet( + "getaddressinfo", + [ + addr_int2, + ], + ) + + key_id_field: str = ( + "hdmasterkeyid" + if "hdmasterkeyid" in addr1_info + else "hdmasterfingerprint" + ) + assert addr1_info[key_id_field] == addr2_info[key_id_field] + assert addr_int1_info[key_id_field] == addr_int2_info[key_id_field] + assert addr1_info[key_id_field] == addr_int1_info[key_id_field] + assert original_seed_id == after_seed_id + finally: + ci.setActiveWallet("wallet.dat") + chain_client_settings["manage_daemon"] = False + def test_01_0_lock_bad_prevouts(self): logging.info( "---------- Test {} lock_bad_prevouts".format(self.test_coin_from.name) diff --git a/tests/basicswap/test_reload.py b/tests/basicswap/test_reload.py index 899cfa1..b3b47cc 100644 --- a/tests/basicswap/test_reload.py +++ b/tests/basicswap/test_reload.py @@ -24,9 +24,6 @@ import threading import multiprocessing from unittest.mock import patch -from basicswap.rpc import ( - callrpc_cli, -) from tests.basicswap.util import ( read_json_api, post_json_api, @@ -38,6 +35,7 @@ from tests.basicswap.common import ( waitForNumSwapping, ) from tests.basicswap.common_xmr import ( + callrpc_cli, prepare_nodes, ) import basicswap.bin.run as runSystem diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index bd55bd7..4404f82 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -39,7 +39,6 @@ from basicswap.util.address import ( ) from basicswap.rpc import ( callrpc, - callrpc_cli, ) from basicswap.rpc_xmr import ( callrpc_xmr, @@ -61,6 +60,7 @@ from tests.basicswap.util import ( read_json_api, ) from tests.basicswap.common import ( + callrpc_cli, prepareDataDir, make_rpc_func, checkForks, @@ -376,7 +376,7 @@ class BaseTest(unittest.TestCase): if os.path.isdir(TEST_DIR): if RESET_TEST: - logging.info("Removing " + TEST_DIR) + logging.info("Removing test dir " + TEST_DIR) for name in os.listdir(TEST_DIR): if name == "pivx-params": continue @@ -388,6 +388,8 @@ class BaseTest(unittest.TestCase): else: logging.info("Restoring instance from " + TEST_DIR) cls.restore_instance = True + else: + logging.info("Creating test dir " + TEST_DIR) if not os.path.exists(TEST_DIR): os.makedirs(TEST_DIR) @@ -399,19 +401,25 @@ class BaseTest(unittest.TestCase): try: logging.info("Preparing coin nodes.") + part_wallet_bin = "particl-wallet" + (".exe" if os.name == "nt" else "") for i in range(NUM_NODES): if not cls.restore_instance: data_dir = prepareDataDir(TEST_DIR, i, "particl.conf", "part_") - if os.path.exists( - os.path.join(cfg.PARTICL_BINDIR, "particl-wallet") + if not os.path.exists( + os.path.join( + cfg.PARTICL_BINDIR, + part_wallet_bin, + ) ): + logging.warning(f"{part_wallet_bin} not found.") + else: try: callrpc_cli( cfg.PARTICL_BINDIR, data_dir, "regtest", "-wallet=wallet.dat -legacy create", - "particl-wallet", + part_wallet_bin, ) except Exception as e: logging.warning( @@ -422,7 +430,7 @@ class BaseTest(unittest.TestCase): data_dir, "regtest", "-wallet=wallet.dat create", - "particl-wallet", + part_wallet_bin, ) cls.part_daemons.append( @@ -471,6 +479,7 @@ class BaseTest(unittest.TestCase): ) rpc("reservebalance", [False]) + btc_wallet_bin = "bitcoin-wallet" + (".exe" if os.name == "nt" else "") for i in range(NUM_BTC_NODES): if not cls.restore_instance: data_dir = prepareDataDir( @@ -481,9 +490,14 @@ class BaseTest(unittest.TestCase): base_p2p_port=BTC_BASE_PORT, base_rpc_port=BTC_BASE_RPC_PORT, ) - if os.path.exists( - os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet") + if not os.path.exists( + os.path.join( + cfg.BITCOIN_BINDIR, + btc_wallet_bin, + ) ): + logging.warning(f"{btc_wallet_bin} not found.") + else: if BTC_USE_DESCRIPTORS: # How to set blank and disable_private_keys with wallet util? pass @@ -494,7 +508,7 @@ class BaseTest(unittest.TestCase): data_dir, "regtest", "-wallet=wallet.dat -legacy create", - "bitcoin-wallet", + btc_wallet_bin, ) except Exception as e: logging.warning( @@ -505,7 +519,7 @@ class BaseTest(unittest.TestCase): data_dir, "regtest", "-wallet=wallet.dat create", - "bitcoin-wallet", + btc_wallet_bin, ) cls.btc_daemons.append( @@ -536,6 +550,7 @@ class BaseTest(unittest.TestCase): ) if cls.start_ltc_nodes: + ltc_wallet_bin = "litecoin-wallet" + (".exe" if os.name == "nt" else "") for i in range(NUM_LTC_NODES): if not cls.restore_instance: data_dir = prepareDataDir( @@ -547,14 +562,16 @@ class BaseTest(unittest.TestCase): base_rpc_port=LTC_BASE_RPC_PORT, ) if os.path.exists( - os.path.join(cfg.LITECOIN_BINDIR, "litecoin-wallet") + os.path.join(cfg.LITECOIN_BINDIR, ltc_wallet_bin) ): + logging.warning(f"{ltc_wallet_bin} not found.") + else: callrpc_cli( cfg.LITECOIN_BINDIR, data_dir, "regtest", "-wallet=wallet.dat create", - "litecoin-wallet", + ltc_wallet_bin, ) cls.ltc_daemons.append( @@ -620,9 +637,10 @@ class BaseTest(unittest.TestCase): "create_wallet", {"filename": "testwallet", "language": "English"}, ) - cls.callxmrnodewallet( - cls, i, "open_wallet", {"filename": "testwallet"} - ) + else: + cls.callxmrnodewallet( + cls, i, "open_wallet", {"filename": "testwallet"} + ) for i in range(NUM_NODES): # Hook for descendant classes