From 2d88491d48fbba64daae81f1084e9b199c0adc09 Mon Sep 17 00:00:00 2001 From: nahuhh <50635951+nahuhh@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:58:00 +0100 Subject: [PATCH 1/2] xmr: detect corrupt wallets --- basicswap/basicswap.py | 4 +++- basicswap/interface/wow.py | 24 ++++++++++++++++++++++++ basicswap/interface/xmr.py | 9 +++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 7fd46de..66de309 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1054,7 +1054,9 @@ class BasicSwap(BaseApp): elif c in (Coins.XMR, Coins.WOW): try: ci.ensureWalletExists() - except Exception as e: # noqa: F841 + except Exception as e: + if "invalid signature" in str(e): # wallet is corrupt + raise self.log.warning( f"Can't open {ci.coin_name()} wallet, could be locked." ) diff --git a/basicswap/interface/wow.py b/basicswap/interface/wow.py index d615b01..87dbc75 100644 --- a/basicswap/interface/wow.py +++ b/basicswap/interface/wow.py @@ -30,3 +30,27 @@ class WOWInterface(XMRInterface): @staticmethod def depth_spendable() -> int: return 3 + + # below only needed until wow is rebased to monero v0.18.4.0+ + def openWallet(self, filename): + params = {"filename": filename} + if self._wallet_password is not None: + params["password"] = self._wallet_password + + try: + self.rpc_wallet("open_wallet", params) + except Exception as e: + if "no connection to daemon" in str(e): + self._log.debug(f"{self.coin_name()} {e}") + return # bypass refresh error to allow startup with a busy daemon + + try: + # TODO Remove `store` after upstream fix to autosave on close_wallet + self.rpc_wallet("store") + self.rpc_wallet("close_wallet") + self._log.debug(f"Attempt to save and close {self.coin_name()} wallet") + except Exception as e: # noqa: F841 + pass + + self.rpc_wallet("open_wallet", params) + self._log.debug(f"Reattempt to open {self.coin_name()} wallet") diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 32bb01b..f6bd3c4 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -205,17 +205,18 @@ class XMRInterface(CoinInterface): if "no connection to daemon" in str(e): self._log.debug(f"{self.coin_name()} {e}") return # bypass refresh error to allow startup with a busy daemon + if "invalid signature" in str(e): + self._log.debug(f"{self.coin_name()} wallet is corrupt") + raise try: - # TODO Remove `store` after upstream fix to autosave on close_wallet - self.rpc_wallet("store") self.rpc_wallet("close_wallet") - self._log.debug(f"Attempt to save and close {self.coin_name()} wallet") + self._log.debug(f"Closing {self.coin_name()} wallet") except Exception as e: # noqa: F841 pass self.rpc_wallet("open_wallet", params) - self._log.debug(f"Reattempt to open {self.coin_name()} wallet") + self._log.debug(f"Attempting to open {self.coin_name()} wallet") def initialiseWallet( self, key_view: bytes, key_spend: bytes, restore_height=None From 9387c43ff5268d2c4e80d8caaa0e67132d123ccd Mon Sep 17 00:00:00 2001 From: tecnovert Date: Mon, 14 Apr 2025 16:59:03 +0200 Subject: [PATCH 2/2] Rename wallet file on error. --- basicswap/interface/xmr.py | 56 ++++++++++++++++++++++++++++++------- tests/basicswap/test_xmr.py | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index f6bd3c4..cf6ce73 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -7,6 +7,7 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import logging +import os import basicswap.contrib.ed25519_fast as edf import basicswap.ed25519_fast_util as edu @@ -204,16 +205,51 @@ class XMRInterface(CoinInterface): except Exception as e: if "no connection to daemon" in str(e): self._log.debug(f"{self.coin_name()} {e}") - return # bypass refresh error to allow startup with a busy daemon - if "invalid signature" in str(e): - self._log.debug(f"{self.coin_name()} wallet is corrupt") - raise - - try: - self.rpc_wallet("close_wallet") - self._log.debug(f"Closing {self.coin_name()} wallet") - except Exception as e: # noqa: F841 - pass + return # Bypass refresh error to allow startup with a busy daemon + if any( + x in str(e) + for x in ( + "invalid signature", + "std::bad_alloc", + "basic_string::_M_replace_aux", + ) + ): + self._log.error(f"{self.coin_name()} wallet is corrupt.") + chain_client_settings = self._sc.getChainClientSettings( + self.coin_type() + ) # basicswap.json + if chain_client_settings.get("manage_wallet_daemon", False): + self._log.info(f"Renaming {self.coin_name()} wallet cache file.") + walletpath = os.path.join( + chain_client_settings.get("datadir", "none"), + "wallets", + filename, + ) + if not os.path.isfile(walletpath): + self._log.warning( + f"Could not find {self.coin_name()} wallet cache file." + ) + raise + bkp_path = walletpath + ".corrupt" + for i in range(100): + if not os.path.exists(bkp_path): + break + bkp_path = walletpath + f".corrupt{i}" + if os.path.exists(bkp_path): + self._log.error( + f"Could not find backup path for {self.coin_name()} wallet." + ) + raise + os.rename(walletpath, bkp_path) + # Drop through to open_wallet + else: + raise + else: + try: + self.rpc_wallet("close_wallet") + self._log.debug(f"Closing {self.coin_name()} wallet") + except Exception as e: # noqa: F841 + pass self.rpc_wallet("open_wallet", params) self._log.debug(f"Attempting to open {self.coin_name()} wallet") diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index a85c7db..26aa476 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -1089,6 +1089,62 @@ class Test(BaseTest): def notest_00_delay(self): test_delay_event.wait(100000) + def test_007_corrupt_wallet(self): + logging.info(f"---------- Test {Coins.XMR.name} corrupt wallet") + swap_clients = self.swap_clients + ci = swap_clients[0].ci(Coins.XMR) + + chain_client_settings = swap_clients[0].getChainClientSettings(Coins.XMR) + wallet_name = chain_client_settings["wallet_name"] + try: + ci.rpc_wallet("close_wallet") + logging.info(f"Closing {ci.coin_name()} wallet") + except Exception as e: + logging.info(f"Closing {ci.coin_name()} wallet failed with: {e}") + + walletpath = os.path.join( + chain_client_settings.get("datadir", "none"), "wallets", wallet_name + ) + wallet_cache_bytes = os.path.getsize(walletpath) + logging.info(f"[rm] wallet_cache_bytes {wallet_cache_bytes}") + logging.info(f"[rm] walletpath {walletpath}") + shutil.copy(walletpath, walletpath + ".orig") + + # Failed to open wallet : basic_string::_M_replace_aux + # with open(walletpath, "wb") as fp: + # fp.write(os.urandom(wallet_cache_bytes)) + + # Failed to open wallet : std::bad_alloc + with open(walletpath, "ab") as fp: + fp.write(os.urandom(1000)) + + # TODO: Get "invalid signature" + + try: + ci.openWallet(wallet_name) + except Exception as e: + logging.info(f"Opening {ci.coin_name()} wallet failed with: {e}") + assert any( + x in str(e) + for x in ( + "invalid signature", + "std::bad_alloc", + "basic_string::_M_replace_aux", + ) + ) + else: + raise ValueError("Should fail!") + + try: + chain_client_settings["manage_wallet_daemon"] = True + try: + ci.openWallet(wallet_name) + except Exception as e: + logging.info(f"Opening {ci.coin_name()} wallet failed with: {e}") + raise + finally: + chain_client_settings["manage_wallet_daemon"] = False + def test_010_txn_size(self): logging.info("---------- Test {} txn_size".format(Coins.PART))