Add BTC descriptor wallet support.

Set BTC_USE_DESCRIPTORS env var to true to enable descriptors in the prepare script and test_btc_xmr
A separate watchonly wallet is created when using descriptor wallets.
This commit is contained in:
tecnovert
2025-01-29 10:16:07 +02:00
parent 4ae97790aa
commit 37be3bcab5
9 changed files with 423 additions and 59 deletions

View File

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

View File

@@ -6,26 +6,23 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import sys
import json
import logging
import multiprocessing
import os
import shutil
import signal
import logging
import unittest
import sys
import threading
import multiprocessing
import unittest
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)

View File

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

View File

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