Merge pull request #297 from tecnovert/wallet_encryption

Add workaround for btc seed changing after encrypting wallet.
This commit is contained in:
tecnovert
2025-04-14 18:03:29 +00:00
committed by GitHub
26 changed files with 1903 additions and 123 deletions

4
.gitignore vendored
View File

@@ -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
compose-dev.yaml

View File

@@ -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}")

View File

@@ -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.")

View File

@@ -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)

View File

@@ -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])

View File

@@ -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])

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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):

View File

@@ -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"])

View File

@@ -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 != "":

View File

@@ -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,
}
)

View File

@@ -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"):

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
)

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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):

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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