diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 6598f1a..2f8b432 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -599,7 +599,12 @@ class BasicSwap(BaseApp): } # Passthrough settings - for setting_name in ("wallet_name", "mweb_wallet_name"): + for setting_name in ( + "use_descriptors", + "wallet_name", + "watch_wallet_name", + "mweb_wallet_name", + ): if setting_name in chain_client_settings: self.coin_clients[coin][setting_name] = chain_client_settings[ setting_name diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index a01f9a1..1c11aff 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -1884,6 +1884,10 @@ def initialise_wallets( if c in (Coins.BTC, Coins.LTC, Coins.DOGE, Coins.DASH): # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors + + use_descriptors = coin_settings.get( + "use_descriptors", False + ) swap_client.callcoinrpc( c, "createwallet", @@ -1893,9 +1897,22 @@ def initialise_wallets( True, WALLET_ENCRYPTION_PWD, False, - False, + use_descriptors, ], ) + if use_descriptors: + swap_client.callcoinrpc( + c, + "createwallet", + [ + coin_settings["watch_wallet_name"], + True, + True, + "", + False, + use_descriptors, + ], + ) swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD) else: swap_client.callcoinrpc( @@ -2538,6 +2555,17 @@ def main(): if set_name != default_name: coin_settings["wallet_name"] = set_name + ticker: str = coin_params["ticker"] + if toBool(os.getenv(ticker + "_USE_DESCRIPTORS", False)): + + if coin_id not in (Coins.BTC,): + raise ValueError(f"Descriptor wallet unavailable for {coin_name}") + + coin_settings["use_descriptors"] = True + coin_settings["watch_wallet_name"] = getWalletName( + coin_params, "bsx_watch", prefix_override=f"{ticker}_WATCH" + ) + if PART_RPC_USER != "": chainclients["particl"]["rpcuser"] = PART_RPC_USER chainclients["particl"]["rpcpassword"] = PART_RPC_PWD diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 07b2376..188e7d5 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -91,6 +91,8 @@ chainparams = { "bip44": 0, "min_amount": 100000, "max_amount": 10000000 * COIN, + "ext_public_key_prefix": 0x0488B21E, + "ext_secret_key_prefix": 0x0488ADE4, }, "testnet": { "rpcport": 18332, @@ -102,6 +104,8 @@ chainparams = { "min_amount": 100000, "max_amount": 10000000 * COIN, "name": "testnet3", + "ext_public_key_prefix": 0x043587CF, + "ext_secret_key_prefix": 0x04358394, }, "regtest": { "rpcport": 18443, @@ -112,6 +116,8 @@ chainparams = { "bip44": 1, "min_amount": 100000, "max_amount": 10000000 * COIN, + "ext_public_key_prefix": 0x043587CF, + "ext_secret_key_prefix": 0x04358394, }, }, Coins.LTC: { diff --git a/basicswap/contrib/test_framework/descriptors.py b/basicswap/contrib/test_framework/descriptors.py new file mode 100644 index 0000000..46b4057 --- /dev/null +++ b/basicswap/contrib/test_framework/descriptors.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 Pieter Wuille +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Utility functions related to output descriptors""" + +import re + +INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " +CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +GENERATOR = [0xf5dee51989, 0xa9fdca3312, 0x1bab10e32d, 0x3706b1677a, 0x644d626ffd] + +def descsum_polymod(symbols): + """Internal function that computes the descriptor checksum.""" + chk = 1 + for value in symbols: + top = chk >> 35 + chk = (chk & 0x7ffffffff) << 5 ^ value + for i in range(5): + chk ^= GENERATOR[i] if ((top >> i) & 1) else 0 + return chk + +def descsum_expand(s): + """Internal function that does the character to symbol expansion""" + groups = [] + symbols = [] + for c in s: + if not c in INPUT_CHARSET: + return None + v = INPUT_CHARSET.find(c) + symbols.append(v & 31) + groups.append(v >> 5) + if len(groups) == 3: + symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2]) + groups = [] + if len(groups) == 1: + symbols.append(groups[0]) + elif len(groups) == 2: + symbols.append(groups[0] * 3 + groups[1]) + return symbols + +def descsum_create(s): + """Add a checksum to a descriptor without""" + symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0] + checksum = descsum_polymod(symbols) ^ 1 + return s + '#' + ''.join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8)) + +def descsum_check(s, require=True): + """Verify that the checksum is correct in a descriptor""" + if not '#' in s: + return not require + if s[-9] != '#': + return False + if not all(x in CHECKSUM_CHARSET for x in s[-8:]): + return False + symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]] + return descsum_polymod(symbols) == 1 + +def drop_origins(s): + '''Drop the key origins from a descriptor''' + desc = re.sub(r'\[.+?\]', '', s) + if '#' in s: + desc = desc[:desc.index('#')] + return descsum_create(desc) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 3041f18..1f9b25b 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -18,12 +18,7 @@ from basicswap.basicswap_util import ( getVoutByAddress, getVoutByScriptPubKey, ) -from basicswap.contrib.test_framework import ( - segwit_addr, -) -from basicswap.interface.base import ( - Secp256k1Interface, -) +from basicswap.interface.base import Secp256k1Interface from basicswap.util import ( ensure, b2h, @@ -35,6 +30,7 @@ from basicswap.util.ecc import ( pointToCPK, CPKToPoint, ) +from basicswap.util.extkey import ExtKeyPair from basicswap.util.script import ( decodeScriptNum, getCompactSizeLen, @@ -44,6 +40,7 @@ from basicswap.util.script import ( from basicswap.util.address import ( toWIF, b58encode, + b58decode, decodeWif, decodeAddress, pubkeyToAddress, @@ -63,6 +60,8 @@ from coincurve.ecdsaotves import ( ecdsaotves_rec_enc_key, ) +from basicswap.contrib.test_framework import segwit_addr +from basicswap.contrib.test_framework.descriptors import descsum_create from basicswap.contrib.test_framework.messages import ( COIN, COutPoint, @@ -267,9 +266,21 @@ class BTCInterface(Secp256k1Interface): self._rpcauth = coin_settings["rpcauth"] self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host) self._rpc_wallet = coin_settings.get("wallet_name", "wallet.dat") + self._rpc_wallet_watch = coin_settings.get( + "watch_wallet_name", self._rpc_wallet + ) self.rpc_wallet = make_rpc_func( self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet ) + if self._rpc_wallet_watch == self._rpc_wallet: + self.rpc_wallet_watch = self.rpc_wallet + else: + self.rpc_wallet_watch = make_rpc_func( + self._rpcport, + self._rpcauth, + host=self._rpc_host, + wallet=self._rpc_wallet_watch, + ) self.blocks_confirmed = coin_settings["blocks_confirmed"] self.setConfTarget(coin_settings["conf_target"]) self._use_segwit = coin_settings["use_segwit"] @@ -278,6 +289,7 @@ class BTCInterface(Secp256k1Interface): self._log = self._sc.log if self._sc and self._sc.log else logging self._expect_seedid_hex = None self._altruistic = coin_settings.get("altruistic", True) + self._use_descriptors = coin_settings.get("use_descriptors", False) def open_rpc(self, wallet=None): return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host) @@ -360,9 +372,40 @@ class BTCInterface(Secp256k1Interface): raise ValueError(f"Block header not found at time: {time}") def initialiseWallet(self, key_bytes: bytes) -> None: - key_wif = self.encodeKey(key_bytes) - self.rpc_wallet("sethdseed", [True, key_wif]) + assert len(key_bytes) == 32 self._have_checked_seed = False + if self._use_descriptors: + self._log.info("Importing descriptors") + ek = ExtKeyPair() + ek.set_seed(key_bytes) + 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", + [ + [ + {"desc": desc_external, "timestamp": "now", "active": True}, + { + "desc": desc_internal, + "timestamp": "now", + "active": True, + "internal": True, + }, + ], + ], + ) + + num_successful: int = 0 + for entry in rv: + if entry.get("success", False) is True: + num_successful += 1 + if num_successful != 2: + self._log.error(f"Failed to import descriptors: {rv}.") + raise ValueError("Failed to import descriptors.") + else: + key_wif = self.encodeKey(key_bytes) + self.rpc_wallet("sethdseed", [True, key_wif]) def getWalletInfo(self): rv = self.rpc_wallet("getwalletinfo") @@ -372,7 +415,14 @@ class BTCInterface(Secp256k1Interface): return rv def getWalletRestoreHeight(self) -> int: - start_time = self.rpc_wallet("getwalletinfo")["keypoololdest"] + if self._use_descriptors: + descriptor = self.getActiveDescriptor() + if descriptor is None: + start_time = 0 + else: + start_time = descriptor["timestamp"] + else: + start_time = self.rpc_wallet("getwalletinfo")["keypoololdest"] blockchaininfo = self.getBlockchainInfo() best_block = blockchaininfo["bestblockhash"] @@ -392,6 +442,8 @@ class BTCInterface(Secp256k1Interface): ) if block_header["time"] < start_time: return block_header["height"] + if "previousblockhash" not in block_header: # Genesis block + return block_header["height"] block_hash = block_header["previousblockhash"] finally: self.close_rpc(rpc_conn) @@ -401,7 +453,32 @@ class BTCInterface(Secp256k1Interface): 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: + if ( + descriptor["desc"].startswith("wpkh") + and descriptor["active"] is True + and descriptor["internal"] is False + ): + return descriptor + return None + def checkExpectedSeed(self, expect_seedid: str) -> bool: + if self._use_descriptors: + descriptor = self.getActiveDescriptor() + if descriptor is None: + self._log.debug("Could not find active descriptor.") + return False + + end = descriptor["desc"].find("/") + if end < 10: + return False + 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 + wallet_seed_id = self.getWalletSeedID() self._expect_seedid_hex = expect_seedid self._have_checked_seed = True @@ -426,6 +503,10 @@ class BTCInterface(Secp256k1Interface): addr_info = self.rpc_wallet("getaddressinfo", [address]) if not or_watch_only: return addr_info["ismine"] + + if self._use_descriptors: + addr_info = self.rpc_wallet_watch("getaddressinfo", [address]) + return addr_info["ismine"] or addr_info["iswatchonly"] def checkAddressMine(self, address: str) -> None: @@ -493,6 +574,20 @@ class BTCInterface(Secp256k1Interface): pkh = hash160(pk) return segwit_addr.encode(bech32_prefix, version, pkh) + def encode_secret_extkey(self, ek_data: bytes) -> str: + assert len(ek_data) == 74 + prefix = self.chainparams_network()["ext_secret_key_prefix"] + data: bytes = prefix.to_bytes(4, "big") + ek_data + checksum = sha256(sha256(data)) + return b58encode(data + checksum[0:4]) + + def encode_public_extkey(self, ek_data: bytes) -> str: + assert len(ek_data) == 74 + prefix = self.chainparams_network()["ext_public_key_prefix"] + data: bytes = prefix.to_bytes(4, "big") + ek_data + checksum = sha256(sha256(data)) + return b58encode(data + checksum[0:4]) + def pkh_to_address(self, pkh: bytes) -> str: # pkh is ripemd160(sha256(pk)) assert len(pkh) == 20 @@ -528,7 +623,12 @@ class BTCInterface(Secp256k1Interface): pk = self.getPubkey(key) return hash160(pk) - def getSeedHash(self, seed) -> bytes: + def getSeedHash(self, seed: bytes) -> bytes: + if self._use_descriptors: + ek = ExtKeyPair() + ek.set_seed(seed) + return hash160(ek.encode_p()) + return self.getAddressHashFromKey(seed)[::-1] def encodeKey(self, key_bytes: bytes) -> str: @@ -1411,7 +1511,7 @@ class BTCInterface(Secp256k1Interface): script_pk = self.getPkDest(Kbs) if locked_n is None: - wtx = self.rpc_wallet( + wtx = self.rpc_wallet_watch( "gettransaction", [ chain_b_lock_txid.hex(), @@ -1448,10 +1548,23 @@ class BTCInterface(Secp256k1Interface): return bytes.fromhex(self.publishTx(b_lock_spend_tx)) - def importWatchOnlyAddress(self, address: str, label: str): + def importWatchOnlyAddress(self, address: str, label: str) -> None: + if self._use_descriptors: + desc_watch = descsum_create(f"addr({address})") + rv = self.rpc_wallet_watch( + "importdescriptors", + [ + [ + {"desc": desc_watch, "timestamp": "now", "active": False}, + ], + ], + ) + ensure(rv[0]["success"] is True, "importdescriptors failed for watchonly") + return + self.rpc_wallet("importaddress", [address, label, False]) - def isWatchOnlyAddress(self, address: str): + def isWatchOnlyAddress(self, address: str) -> bool: addr_info = self.rpc_wallet("getaddressinfo", [address]) return addr_info["iswatchonly"] @@ -1481,7 +1594,7 @@ class BTCInterface(Secp256k1Interface): return_txid = True if txid is None else False if txid is None: - txns = self.rpc_wallet( + txns = self.rpc_wallet_watch( "listunspent", [ 0, @@ -1502,7 +1615,7 @@ class BTCInterface(Secp256k1Interface): try: # set `include_watchonly` explicitly to `True` to get transactions for watchonly addresses also in BCH - tx = self.rpc_wallet("gettransaction", [txid.hex(), True]) + tx = self.rpc_wallet_watch("gettransaction", [txid.hex(), True]) block_height = 0 if "blockhash" in tx: diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 4e9d3d5..f8d1a33 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2020-2024 tecnovert -# Copyright (c) 2024 The Basicswap developers +# Copyright (c) 2024-2025 The Basicswap developers # Distributed under the MIT software license, see the accompanying # file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. @@ -14,6 +14,7 @@ from urllib.request import urlopen from .util import read_json_api from basicswap.rpc import callrpc +from basicswap.util import toBool from basicswap.contrib.rpcauth import generate_salt, password_to_hmac from basicswap.bin.prepare import downloadPIVXParams @@ -44,6 +45,8 @@ PIVX_BASE_ZMQ_PORT = 36892 PREFIX_SECRET_KEY_REGTEST = 0x2E +BTC_USE_DESCRIPTORS = toBool(os.getenv("BTC_USE_DESCRIPTORS", False)) + def prepareDataDir( datadir, diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py index 1324494..97312f1 100644 --- a/tests/basicswap/common_xmr.py +++ b/tests/basicswap/common_xmr.py @@ -19,13 +19,10 @@ from io import StringIO from urllib.request import urlopen from unittest.mock import patch -from basicswap.rpc_xmr import ( - callrpc_xmr, -) +from basicswap.contrib.rpcauth import generate_salt, password_to_hmac +from basicswap.rpc_xmr import callrpc_xmr from tests.basicswap.mnemonics import mnemonics -from tests.basicswap.util import ( - waitForServer, -) +from tests.basicswap.util import waitForServer from tests.basicswap.common import ( BASE_PORT, BASE_RPC_PORT, @@ -35,6 +32,7 @@ from tests.basicswap.common import ( LTC_BASE_PORT, LTC_BASE_RPC_PORT, PIVX_BASE_PORT, + BTC_USE_DESCRIPTORS, ) from tests.basicswap.extended.test_dcr import ( DCR_BASE_PORT, @@ -49,8 +47,6 @@ from tests.basicswap.extended.test_doge import ( DOGE_BASE_RPC_PORT, ) -from basicswap.contrib.rpcauth import generate_salt, password_to_hmac - import basicswap.config as cfg import basicswap.bin.run as runSystem @@ -133,6 +129,7 @@ def run_prepare( os.environ["PART_RPC_PORT"] = str(PARTICL_RPC_PORT_BASE) os.environ["BTC_RPC_PORT"] = str(BITCOIN_RPC_PORT_BASE) os.environ["BTC_PORT"] = str(BITCOIN_PORT_BASE) + os.environ["BTC_USE_DESCRIPTORS"] = str(BTC_USE_DESCRIPTORS) os.environ["LTC_RPC_PORT"] = str(LITECOIN_RPC_PORT_BASE) os.environ["DCR_RPC_PORT"] = str(DECRED_RPC_PORT_BASE) os.environ["FIRO_RPC_PORT"] = str(FIRO_RPC_PORT_BASE) diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 54d0145..35335d8 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -26,6 +26,7 @@ from basicswap.db import ( from basicswap.util import ( make_int, ) +from basicswap.util.extkey import ExtKeyPair from basicswap.interface.base import Curves from tests.basicswap.util import ( read_json_api, @@ -40,6 +41,7 @@ from tests.basicswap.common import ( wait_for_none_active, BTC_BASE_RPC_PORT, ) +from basicswap.contrib.test_framework.descriptors import descsum_create from basicswap.contrib.test_framework.messages import ( ToHex, FromHex, @@ -58,6 +60,8 @@ from .test_xmr import BaseTest, test_delay_event, callnoderpc logger = logging.getLogger() +test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" + class TestFunctions(BaseTest): base_rpc_port = None @@ -1166,7 +1170,6 @@ class BasicSwapTest(TestFunctions): logging.info("---------- Test {} hdwallet".format(self.test_coin_from.name)) ci = self.swap_clients[0].ci(self.test_coin_from) - test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" test_wif = ( self.swap_clients[0] .ci(self.test_coin_from) @@ -1178,10 +1181,35 @@ class BasicSwapTest(TestFunctions): "createwallet", [new_wallet_name, False, True, "", False, False] ) self.callnoderpc("sethdseed", [True, test_wif], wallet=new_wallet_name) + + wi = self.callnoderpc("getwalletinfo", wallet=new_wallet_name) + assert wi["hdseedid"] == "3da5c0af91879e8ce97d9a843874601c08688078" + addr = self.callnoderpc("getnewaddress", wallet=new_wallet_name) - self.callnoderpc("unloadwallet", [new_wallet_name]) + addr_info = self.callnoderpc( + "getaddressinfo", + [ + addr, + ], + wallet=new_wallet_name, + ) + assert addr_info["hdmasterfingerprint"] == "a55b7ea9" + assert addr_info["hdkeypath"] == "m/0'/0'/0'" assert addr == "bcrt1qps7hnjd866e9ynxadgseprkc2l56m00dvwargr" + addr_change = self.callnoderpc("getrawchangeaddress", wallet=new_wallet_name) + addr_info = self.callnoderpc( + "getaddressinfo", + [ + addr_change, + ], + wallet=new_wallet_name, + ) + assert addr_info["hdmasterfingerprint"] == "a55b7ea9" + assert addr_info["hdkeypath"] == "m/0'/1'/0'" + assert addr_change == "bcrt1qdl9ryxkqjltv42lhfnqgdjf9tagxsjpp2xak9a" + self.callnoderpc("unloadwallet", [new_wallet_name]) + self.swap_clients[0].initialiseWallet(Coins.BTC, raise_errors=True) assert self.swap_clients[0].checkWalletSeed(Coins.BTC) is True for i in range(1500): @@ -1561,6 +1589,97 @@ class BasicSwapTest(TestFunctions): ) assert len(tx_wallet["blockhash"]) == 64 + def test_013_descriptor_wallet(self): + logging.info(f"---------- Test {self.test_coin_from.name} descriptor wallet") + + ci = self.swap_clients[0].ci(self.test_coin_from) + + ek = ExtKeyPair() + ek.set_seed(bytes.fromhex(test_seed)) + ek_encoded: str = ci.encode_secret_extkey(ek.encode_v()) + new_wallet_name = "descriptors_" + random.randbytes(10).hex() + new_watch_wallet_name = "watch_descriptors_" + random.randbytes(10).hex() + # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors + ci.rpc("createwallet", [new_wallet_name, False, True, "", False, True]) + ci.rpc("createwallet", [new_watch_wallet_name, True, True, "", False, True]) + + desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)") + desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)") + self.callnoderpc( + "importdescriptors", + [ + [ + { + "desc": desc_external, + "timestamp": "now", + "active": True, + "range": [0, 10], + "next_index": 0, + }, + { + "desc": desc_internal, + "timestamp": "now", + "active": True, + "internal": True, + }, + ], + ], + wallet=new_wallet_name, + ) + + addr = self.callnoderpc("getnewaddress", wallet=new_wallet_name) + addr_info = self.callnoderpc( + "getaddressinfo", + [ + addr, + ], + wallet=new_wallet_name, + ) + assert addr_info["hdmasterfingerprint"] == "a55b7ea9" + assert addr_info["hdkeypath"] == "m/0h/0h/0h" + assert addr == "bcrt1qps7hnjd866e9ynxadgseprkc2l56m00dvwargr" + + addr_change = self.callnoderpc("getrawchangeaddress", wallet=new_wallet_name) + addr_info = self.callnoderpc( + "getaddressinfo", + [ + addr_change, + ], + wallet=new_wallet_name, + ) + assert addr_info["hdmasterfingerprint"] == "a55b7ea9" + assert addr_info["hdkeypath"] == "m/0h/1h/0h" + assert addr_change == "bcrt1qdl9ryxkqjltv42lhfnqgdjf9tagxsjpp2xak9a" + + desc_watch = descsum_create(f"addr({addr})") + self.callnoderpc( + "importdescriptors", + [ + [ + {"desc": desc_watch, "timestamp": "now", "active": False}, + ], + ], + wallet=new_watch_wallet_name, + ) + ci.rpc_wallet("sendtoaddress", [addr, 1]) + found: bool = False + for i in range(10): + txn_list = self.callnoderpc( + "listtransactions", ["*", 100, 0, True], wallet=new_watch_wallet_name + ) + test_delay_event.wait(1) + if len(txn_list) > 0: + found = True + break + assert found + + # Test that addresses can be generated beyond range in listdescriptors + for i in range(2000): + self.callnoderpc("getnewaddress", wallet=new_wallet_name) + + self.callnoderpc("unloadwallet", [new_wallet_name]) + self.callnoderpc("unloadwallet", [new_watch_wallet_name]) + def test_01_0_lock_bad_prevouts(self): logging.info( "---------- Test {} lock_bad_prevouts".format(self.test_coin_from.name) @@ -1862,11 +1981,11 @@ class TestBTC(BasicSwapTest): assert "seed is set from the Basicswap mnemonic" in rv["error"] rv = read_json_api(1800, "getcoinseed", {"coin": "BTC"}) - assert ( - rv["seed"] - == "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" + assert rv["seed"] == test_seed + assert rv["seed_id"] in ( + "3da5c0af91879e8ce97d9a843874601c08688078", + "4a231080ec6f4078e543d39cc6dcf0b922c9b16b", ) - assert rv["seed_id"] == "3da5c0af91879e8ce97d9a843874601c08688078" assert rv["seed_id"] == rv["expected_seed_id"] rv = read_json_api( diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 527f335..f85f8f5 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -83,6 +83,7 @@ from tests.basicswap.common import ( LTC_BASE_PORT, LTC_BASE_RPC_PORT, PREFIX_SECRET_KEY_REGTEST, + BTC_USE_DESCRIPTORS, ) from basicswap.db_util import ( remove_expired_data, @@ -172,6 +173,7 @@ def prepare_swapclient_dir( "datadir": os.path.join(datadir, "btc_" + str(node_id)), "bindir": cfg.BITCOIN_BINDIR, "use_segwit": True, + "use_descriptors": BTC_USE_DESCRIPTORS, }, }, "check_progress_seconds": 2, @@ -189,6 +191,9 @@ def prepare_swapclient_dir( "restrict_unknown_seed_wallets": False, } + if BTC_USE_DESCRIPTORS: + settings["chainclients"]["bitcoin"]["watch_wallet_name"] = "bsx_watch" + if Coins.XMR in with_coins: settings["chainclients"]["monero"] = { "connection_type": "rpc", @@ -474,25 +479,29 @@ class BaseTest(unittest.TestCase): if os.path.exists( os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet") ): - try: - callrpc_cli( - cfg.BITCOIN_BINDIR, - data_dir, - "regtest", - "-wallet=wallet.dat -legacy create", - "bitcoin-wallet", - ) - except Exception as e: - logging.warning( - f"bitcoin-wallet create failed {e}, retrying without -legacy" - ) - callrpc_cli( - cfg.BITCOIN_BINDIR, - data_dir, - "regtest", - "-wallet=wallet.dat create", - "bitcoin-wallet", - ) + if BTC_USE_DESCRIPTORS: + # How to set blank and disable_private_keys with wallet util? + pass + else: + try: + callrpc_cli( + cfg.BITCOIN_BINDIR, + data_dir, + "regtest", + "-wallet=wallet.dat -legacy create", + "bitcoin-wallet", + ) + except Exception as e: + logging.warning( + f"bitcoin-wallet create failed {e}, retrying without -legacy" + ) + callrpc_cli( + cfg.BITCOIN_BINDIR, + data_dir, + "regtest", + "-wallet=wallet.dat create", + "bitcoin-wallet", + ) cls.btc_daemons.append( startDaemon( @@ -505,9 +514,21 @@ class BaseTest(unittest.TestCase): "Started %s %d", cfg.BITCOIND, cls.part_daemons[-1].handle.pid ) - waitForRPC( - make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT), test_delay_event - ) + if BTC_USE_DESCRIPTORS: + rpc_func = make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT) + waitForRPC( + rpc_func, test_delay_event, rpc_command="getblockchaininfo" + ) + # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors + rpc_func( + "createwallet", ["wallet.dat", False, True, "", False, True] + ) + rpc_func("createwallet", ["bsx_watch", True, True, "", False, True]) + else: + waitForRPC( + make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT), + test_delay_event, + ) if cls.start_ltc_nodes: for i in range(NUM_LTC_NODES): @@ -658,6 +679,11 @@ class BaseTest(unittest.TestCase): xmr_ci.getMainWalletAddress(), ) + if BTC_USE_DESCRIPTORS: + # sc.initialiseWallet(Coins.BTC) + # Import a random seed to keep the existing test behaviour. BTC core rescans even with timestamp: now. + sc.ci(Coins.BTC).initialiseWallet(random.randbytes(32)) + t = HttpThread(sc.fp, TEST_HTTP_HOST, TEST_HTTP_PORT + i, False, sc) cls.http_threads.append(t) t.start() @@ -685,6 +711,7 @@ class BaseTest(unittest.TestCase): "getnewaddress", ["mining_addr", "bech32"], base_rpc_port=BTC_BASE_RPC_PORT, + wallet="wallet.dat", ) num_blocks = 400 # Mine enough to activate segwit logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr) @@ -700,6 +727,7 @@ class BaseTest(unittest.TestCase): "getnewaddress", ["initial addr"], base_rpc_port=BTC_BASE_RPC_PORT, + wallet="wallet.dat", ) for i in range(5): callnoderpc( @@ -707,6 +735,7 @@ class BaseTest(unittest.TestCase): "sendtoaddress", [btc_addr1, 100], base_rpc_port=BTC_BASE_RPC_PORT, + wallet="wallet.dat", ) # Switch addresses so wallet amounts stay constant