Merge pull request #252 from tecnovert/descriptors

Add BTC descriptor wallet support.
This commit is contained in:
tecnovert
2025-01-31 19:06:22 +00:00
committed by GitHub
9 changed files with 418 additions and 54 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

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

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