From 247f23cb4a5b0f504358ab52ae77c2e095045aab Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 22 May 2024 09:59:57 +0200 Subject: [PATCH] Integrate Decred with wallet encryption. dcrwallet requires the password to be entered at the first startup when encrypted. basicswap-run with --startonlycoin=decred and the WALLET_ENCRYPTION_PWD environment var set can be used for the initial sync. --- basicswap/basicswap.py | 6 +++ basicswap/interface/btc.py | 2 +- basicswap/interface/dcr/dcr.py | 38 +++++++++++++++++++ bin/basicswap_prepare.py | 9 ++++- bin/basicswap_run.py | 14 +++++-- tests/basicswap/extended/test_dcr.py | 36 +++++++++++++++++- .../basicswap/extended/test_xmr_persistent.py | 7 ++-- 7 files changed, 102 insertions(+), 10 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index c566c45..8871f11 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -6861,6 +6861,12 @@ class BasicSwap(BaseApp): self.ci(coin).setAnonTxRingSize(new_anon_tx_ring_size) break + if 'wallet_pwd' in data: + new_wallet_pwd = data['wallet_pwd'] + if settings_cc.get('wallet_pwd', '') != new_wallet_pwd: + settings_changed = True + settings_cc['wallet_pwd'] = new_wallet_pwd + if settings_changed: settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME) settings_path_new = settings_path + '.new' diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index cd46c05..baded69 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -1379,7 +1379,7 @@ class BTCInterface(Secp256k1Interface): return True return False - def isWalletEncryptedLocked(self): + def isWalletEncryptedLocked(self) -> (bool, bool): wallet_info = self.rpc_wallet('getwalletinfo') encrypted = 'unlocked_until' in wallet_info locked = encrypted and wallet_info['unlocked_until'] <= 0 diff --git a/basicswap/interface/dcr/dcr.py b/basicswap/interface/dcr/dcr.py index 15d2676..3bf17c4 100644 --- a/basicswap/interface/dcr/dcr.py +++ b/basicswap/interface/dcr/dcr.py @@ -320,6 +320,44 @@ class DCRInterface(Secp256k1Interface): # Load with --create pass + def isWalletEncrypted(self) -> bool: + return True + + def isWalletLocked(self) -> bool: + walletislocked = self.rpc_wallet('walletislocked') + return walletislocked + + def isWalletEncryptedLocked(self) -> (bool, bool): + walletislocked = self.rpc_wallet('walletislocked') + return True, walletislocked + + def changeWalletPassword(self, old_password: str, new_password: str): + self._log.info('changeWalletPassword - {}'.format(self.ticker())) + if old_password == '': + # Read initial pwd from settings + settings = self._sc.getChainClientSettings(self.coin_type()) + old_password = settings['wallet_pwd'] + self.rpc_wallet('walletpassphrasechange', [old_password, new_password]) + + # Lock wallet to match other coins + self.rpc_wallet('walletlock') + + # Clear initial password + self._sc.editSettings(self.coin_name().lower(), {'wallet_pwd': ''}) + + def unlockWallet(self, password: str): + if password == '': + return + self._log.info('unlockWallet - {}'.format(self.ticker())) + + # Max timeout value, ~3 years + self.rpc_wallet('walletpassphrase', [password, 100000000]) + self._sc.checkWalletSeed(self.coin_type()) + + def lockWallet(self): + self._log.info('lockWallet - {}'.format(self.ticker())) + self.rpc_wallet('walletlock') + def getWalletSeedID(self): masterpubkey = self.rpc_wallet('getmasterpubkey') masterpubkey_data = self.decode_address(masterpubkey)[4:] diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 713de67..fa52aa0 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -1311,8 +1311,10 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, if c == Coins.DCR: if coin_settings['manage_wallet_daemon']: from basicswap.interface.dcr.util import createDCRWallet + + dcr_password = coin_settings['wallet_pwd'] if WALLET_ENCRYPTION_PWD == '' else WALLET_ENCRYPTION_PWD extra_opts = ['--appdata="{}"'.format(coin_settings['datadir']), - '--pass={}'.format(coin_settings['wallet_pwd']), + '--pass={}'.format(dcr_password), ] filename = 'dcrwallet' + ('.exe' if os.name == 'nt' else '') @@ -1381,6 +1383,9 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, else: print(f'WARNING - Failed to initialise wallet for {getCoinName(c)}: {e}') + if 'decred' in with_coins and WALLET_ENCRYPTION_PWD != '': + print('WARNING - dcrwallet requires the password to be entered at the first startup when encrypted.\nPlease use basicswap-run with --startonlycoin=decred and the WALLET_ENCRYPTION_PWD environment var set for the initial sync.') + if particl_wallet_mnemonic is not None: if particl_wallet_mnemonic: # Print directly to stdout for tests @@ -1693,7 +1698,7 @@ def main(): 'connection_type': 'rpc' if 'decred' in with_coins else 'none', 'manage_daemon': True if ('decred' in with_coins and DCR_RPC_HOST == '127.0.0.1') else False, 'manage_wallet_daemon': True if ('decred' in with_coins and DCR_WALLET_RPC_HOST == '127.0.0.1') else False, - 'wallet_pwd': DCR_WALLET_PWD, + 'wallet_pwd': DCR_WALLET_PWD if WALLET_ENCRYPTION_PWD == '' else '', 'rpchost': DCR_RPC_HOST, 'rpcport': DCR_RPC_PORT + port_offset, 'walletrpchost': DCR_WALLET_RPC_HOST, diff --git a/bin/basicswap_run.py b/bin/basicswap_run.py index 1a4564b..1217ab7 100755 --- a/bin/basicswap_run.py +++ b/bin/basicswap_run.py @@ -40,7 +40,7 @@ class Daemon: def is_known_coin(coin_name: str) -> bool: - for k, v in chainparams: + for k, v in chainparams.items(): if coin_name == v['name']: return True return False @@ -169,7 +169,11 @@ def runClient(fp, data_dir, chain, start_only_coins): pids_path = os.path.join(data_dir, '.pids') if os.getenv('WALLET_ENCRYPTION_PWD', '') != '': - raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.') + if 'decred' in start_only_coins: + # Workaround for dcrwallet requiring password for initial startup + logger.warning('Allowing set WALLET_ENCRYPTION_PWD var with --startonlycoin=decred.') + else: + raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.') if not os.path.exists(settings_path): raise ValueError('Settings file not found: ' + str(settings_path)) @@ -255,7 +259,11 @@ def runClient(fp, data_dir, chain, start_only_coins): filename = 'dcrwallet' + ('.exe' if os.name == 'nt' else '') wallet_pwd = v['wallet_pwd'] - extra_opts.append(f'--pass="{wallet_pwd}"') + if wallet_pwd == '': + # Only set when in startonlycoin mode + wallet_pwd = os.getenv('WALLET_ENCRYPTION_PWD', '') + if wallet_pwd != '': + extra_opts.append(f'--pass="{wallet_pwd}"') extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrwallet_stdout.log'} daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config)) pid = daemons[-1].handle.pid diff --git a/tests/basicswap/extended/test_dcr.py b/tests/basicswap/extended/test_dcr.py index 16a33b3..b68c2d7 100644 --- a/tests/basicswap/extended/test_dcr.py +++ b/tests/basicswap/extended/test_dcr.py @@ -594,6 +594,7 @@ class Test(BaseTest): 'rpcpassword': 'test_pass' + str(node_id), 'datadir': os.path.join(datadir, 'dcr_' + str(node_id)), 'bindir': DCR_BINDIR, + 'wallet_pwd': 'test_pass', 'use_csv': True, 'use_segwit': True, 'blocks_confirmed': 1, @@ -942,8 +943,41 @@ class Test(BaseTest): amount_proved = ci0.verifyProofOfFunds(funds_proof[0], funds_proof[1], funds_proof[2], 'test'.encode('utf-8')) assert (amount_proved >= require_amount) + def test_009_wallet_encryption(self): + logging.info('---------- Test {} wallet encryption'.format(self.test_coin.name)) + + for coin in ('part', 'dcr', 'xmr'): + jsw = read_json_api(1800, f'wallets/{coin}') + assert (jsw['encrypted'] is (True if coin == 'dcr' else False)) + assert (jsw['locked'] is False) + + read_json_api(1800, 'setpassword', {'oldpassword': '', 'newpassword': 'notapassword123'}) + + # Entire system is locked with Particl wallet + jsw = read_json_api(1800, 'wallets/dcr') + assert ('Coin must be unlocked' in jsw['error']) + + read_json_api(1800, 'unlock', {'coin': 'part', 'password': 'notapassword123'}) + + for coin in ('dcr', 'xmr'): + jsw = read_json_api(1800, f'wallets/{coin}') + assert (jsw['encrypted'] is True) + assert (jsw['locked'] is True) + + read_json_api(1800, 'lock', {'coin': 'part'}) + jsw = read_json_api(1800, 'wallets/part') + assert ('Coin must be unlocked' in jsw['error']) + + read_json_api(1800, 'setpassword', {'oldpassword': 'notapassword123', 'newpassword': 'notapassword456'}) + read_json_api(1800, 'unlock', {'password': 'notapassword456'}) + + for coin in ('part', 'dcr', 'xmr'): + jsw = read_json_api(1800, f'wallets/{coin}') + assert (jsw['encrypted'] is True) + assert (jsw['locked'] is False) + def test_010_txn_size(self): - logging.info('---------- Test {} txn_size'.format(self.test_coin.name)) + logging.info('---------- Test {} txn size'.format(self.test_coin.name)) swap_clients = self.swap_clients ci = swap_clients[0].ci(self.test_coin) diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py index c0a6057..ffd0781 100644 --- a/tests/basicswap/extended/test_xmr_persistent.py +++ b/tests/basicswap/extended/test_xmr_persistent.py @@ -250,9 +250,10 @@ class Test(unittest.TestCase): self.update_thread_dcr = threading.Thread(target=updateThreadDCR, args=(self,)) self.update_thread_dcr.start() - # Lower output split threshold for more stakeable outputs - for i in range(NUM_NODES): - callpartrpc(i, 'walletsettings', ['stakingoptions', {'stakecombinethreshold': 100, 'stakesplitthreshold': 200}]) + if RESET_TEST: + # Lower output split threshold for more stakeable outputs + for i in range(NUM_NODES): + callpartrpc(i, 'walletsettings', ['stakingoptions', {'stakecombinethreshold': 100, 'stakesplitthreshold': 200}]) self.update_thread = threading.Thread(target=updateThread, args=(self,)) self.update_thread.start()