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

@@ -599,7 +599,12 @@ class BasicSwap(BaseApp):
} }
# Passthrough settings # 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: if setting_name in chain_client_settings:
self.coin_clients[coin][setting_name] = chain_client_settings[ self.coin_clients[coin][setting_name] = chain_client_settings[
setting_name setting_name

View File

@@ -1884,6 +1884,10 @@ def initialise_wallets(
if c in (Coins.BTC, Coins.LTC, Coins.DOGE, Coins.DASH): if c in (Coins.BTC, Coins.LTC, Coins.DOGE, Coins.DASH):
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
use_descriptors = coin_settings.get(
"use_descriptors", False
)
swap_client.callcoinrpc( swap_client.callcoinrpc(
c, c,
"createwallet", "createwallet",
@@ -1893,9 +1897,22 @@ def initialise_wallets(
True, True,
WALLET_ENCRYPTION_PWD, WALLET_ENCRYPTION_PWD,
False, 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) swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
else: else:
swap_client.callcoinrpc( swap_client.callcoinrpc(
@@ -2538,6 +2555,17 @@ def main():
if set_name != default_name: if set_name != default_name:
coin_settings["wallet_name"] = set_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 != "": if PART_RPC_USER != "":
chainclients["particl"]["rpcuser"] = PART_RPC_USER chainclients["particl"]["rpcuser"] = PART_RPC_USER
chainclients["particl"]["rpcpassword"] = PART_RPC_PWD chainclients["particl"]["rpcpassword"] = PART_RPC_PWD

View File

@@ -91,6 +91,8 @@ chainparams = {
"bip44": 0, "bip44": 0,
"min_amount": 100000, "min_amount": 100000,
"max_amount": 10000000 * COIN, "max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x0488B21E,
"ext_secret_key_prefix": 0x0488ADE4,
}, },
"testnet": { "testnet": {
"rpcport": 18332, "rpcport": 18332,
@@ -102,6 +104,8 @@ chainparams = {
"min_amount": 100000, "min_amount": 100000,
"max_amount": 10000000 * COIN, "max_amount": 10000000 * COIN,
"name": "testnet3", "name": "testnet3",
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
}, },
"regtest": { "regtest": {
"rpcport": 18443, "rpcport": 18443,
@@ -112,6 +116,8 @@ chainparams = {
"bip44": 1, "bip44": 1,
"min_amount": 100000, "min_amount": 100000,
"max_amount": 10000000 * COIN, "max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
}, },
}, },
Coins.LTC: { Coins.LTC: {

View File

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

View File

@@ -18,12 +18,7 @@ from basicswap.basicswap_util import (
getVoutByAddress, getVoutByAddress,
getVoutByScriptPubKey, getVoutByScriptPubKey,
) )
from basicswap.contrib.test_framework import ( from basicswap.interface.base import Secp256k1Interface
segwit_addr,
)
from basicswap.interface.base import (
Secp256k1Interface,
)
from basicswap.util import ( from basicswap.util import (
ensure, ensure,
b2h, b2h,
@@ -35,6 +30,7 @@ from basicswap.util.ecc import (
pointToCPK, pointToCPK,
CPKToPoint, CPKToPoint,
) )
from basicswap.util.extkey import ExtKeyPair
from basicswap.util.script import ( from basicswap.util.script import (
decodeScriptNum, decodeScriptNum,
getCompactSizeLen, getCompactSizeLen,
@@ -44,6 +40,7 @@ from basicswap.util.script import (
from basicswap.util.address import ( from basicswap.util.address import (
toWIF, toWIF,
b58encode, b58encode,
b58decode,
decodeWif, decodeWif,
decodeAddress, decodeAddress,
pubkeyToAddress, pubkeyToAddress,
@@ -63,6 +60,8 @@ from coincurve.ecdsaotves import (
ecdsaotves_rec_enc_key, 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 ( from basicswap.contrib.test_framework.messages import (
COIN, COIN,
COutPoint, COutPoint,
@@ -267,9 +266,21 @@ class BTCInterface(Secp256k1Interface):
self._rpcauth = coin_settings["rpcauth"] self._rpcauth = coin_settings["rpcauth"]
self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host) 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 = 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.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet 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.blocks_confirmed = coin_settings["blocks_confirmed"]
self.setConfTarget(coin_settings["conf_target"]) self.setConfTarget(coin_settings["conf_target"])
self._use_segwit = coin_settings["use_segwit"] 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._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None self._expect_seedid_hex = None
self._altruistic = coin_settings.get("altruistic", True) self._altruistic = coin_settings.get("altruistic", True)
self._use_descriptors = coin_settings.get("use_descriptors", False)
def open_rpc(self, wallet=None): def open_rpc(self, wallet=None):
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host) 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}") raise ValueError(f"Block header not found at time: {time}")
def initialiseWallet(self, key_bytes: bytes) -> None: def initialiseWallet(self, key_bytes: bytes) -> None:
key_wif = self.encodeKey(key_bytes) assert len(key_bytes) == 32
self.rpc_wallet("sethdseed", [True, key_wif])
self._have_checked_seed = False 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): def getWalletInfo(self):
rv = self.rpc_wallet("getwalletinfo") rv = self.rpc_wallet("getwalletinfo")
@@ -372,7 +415,14 @@ class BTCInterface(Secp256k1Interface):
return rv return rv
def getWalletRestoreHeight(self) -> int: 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() blockchaininfo = self.getBlockchainInfo()
best_block = blockchaininfo["bestblockhash"] best_block = blockchaininfo["bestblockhash"]
@@ -392,6 +442,8 @@ class BTCInterface(Secp256k1Interface):
) )
if block_header["time"] < start_time: if block_header["time"] < start_time:
return block_header["height"] return block_header["height"]
if "previousblockhash" not in block_header: # Genesis block
return block_header["height"]
block_hash = block_header["previousblockhash"] block_hash = block_header["previousblockhash"]
finally: finally:
self.close_rpc(rpc_conn) self.close_rpc(rpc_conn)
@@ -401,7 +453,32 @@ class BTCInterface(Secp256k1Interface):
wi = self.rpc_wallet("getwalletinfo") wi = self.rpc_wallet("getwalletinfo")
return "Not found" if "hdseedid" not in wi else wi["hdseedid"] 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: 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() wallet_seed_id = self.getWalletSeedID()
self._expect_seedid_hex = expect_seedid self._expect_seedid_hex = expect_seedid
self._have_checked_seed = True self._have_checked_seed = True
@@ -426,6 +503,10 @@ class BTCInterface(Secp256k1Interface):
addr_info = self.rpc_wallet("getaddressinfo", [address]) addr_info = self.rpc_wallet("getaddressinfo", [address])
if not or_watch_only: if not or_watch_only:
return addr_info["ismine"] return addr_info["ismine"]
if self._use_descriptors:
addr_info = self.rpc_wallet_watch("getaddressinfo", [address])
return addr_info["ismine"] or addr_info["iswatchonly"] return addr_info["ismine"] or addr_info["iswatchonly"]
def checkAddressMine(self, address: str) -> None: def checkAddressMine(self, address: str) -> None:
@@ -493,6 +574,20 @@ class BTCInterface(Secp256k1Interface):
pkh = hash160(pk) pkh = hash160(pk)
return segwit_addr.encode(bech32_prefix, version, pkh) 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: def pkh_to_address(self, pkh: bytes) -> str:
# pkh is ripemd160(sha256(pk)) # pkh is ripemd160(sha256(pk))
assert len(pkh) == 20 assert len(pkh) == 20
@@ -528,7 +623,12 @@ class BTCInterface(Secp256k1Interface):
pk = self.getPubkey(key) pk = self.getPubkey(key)
return hash160(pk) 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] return self.getAddressHashFromKey(seed)[::-1]
def encodeKey(self, key_bytes: bytes) -> str: def encodeKey(self, key_bytes: bytes) -> str:
@@ -1411,7 +1511,7 @@ class BTCInterface(Secp256k1Interface):
script_pk = self.getPkDest(Kbs) script_pk = self.getPkDest(Kbs)
if locked_n is None: if locked_n is None:
wtx = self.rpc_wallet( wtx = self.rpc_wallet_watch(
"gettransaction", "gettransaction",
[ [
chain_b_lock_txid.hex(), chain_b_lock_txid.hex(),
@@ -1448,10 +1548,23 @@ class BTCInterface(Secp256k1Interface):
return bytes.fromhex(self.publishTx(b_lock_spend_tx)) 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]) 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]) addr_info = self.rpc_wallet("getaddressinfo", [address])
return addr_info["iswatchonly"] return addr_info["iswatchonly"]
@@ -1481,7 +1594,7 @@ class BTCInterface(Secp256k1Interface):
return_txid = True if txid is None else False return_txid = True if txid is None else False
if txid is None: if txid is None:
txns = self.rpc_wallet( txns = self.rpc_wallet_watch(
"listunspent", "listunspent",
[ [
0, 0,
@@ -1502,7 +1615,7 @@ class BTCInterface(Secp256k1Interface):
try: try:
# set `include_watchonly` explicitly to `True` to get transactions for watchonly addresses also in BCH # 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 block_height = 0
if "blockhash" in tx: if "blockhash" in tx:

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE.txt or http://www.opensource.org/licenses/mit-license.php. # 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 .util import read_json_api
from basicswap.rpc import callrpc from basicswap.rpc import callrpc
from basicswap.util import toBool
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from basicswap.bin.prepare import downloadPIVXParams from basicswap.bin.prepare import downloadPIVXParams
@@ -44,6 +45,8 @@ PIVX_BASE_ZMQ_PORT = 36892
PREFIX_SECRET_KEY_REGTEST = 0x2E PREFIX_SECRET_KEY_REGTEST = 0x2E
BTC_USE_DESCRIPTORS = toBool(os.getenv("BTC_USE_DESCRIPTORS", False))
def prepareDataDir( def prepareDataDir(
datadir, datadir,

View File

@@ -6,26 +6,23 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import sys
import json import json
import logging
import multiprocessing
import os
import shutil import shutil
import signal import signal
import logging import sys
import unittest
import threading import threading
import multiprocessing import unittest
from io import StringIO from io import StringIO
from urllib.request import urlopen from urllib.request import urlopen
from unittest.mock import patch from unittest.mock import patch
from basicswap.rpc_xmr import ( from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
callrpc_xmr, from basicswap.rpc_xmr import callrpc_xmr
)
from tests.basicswap.mnemonics import mnemonics from tests.basicswap.mnemonics import mnemonics
from tests.basicswap.util import ( from tests.basicswap.util import waitForServer
waitForServer,
)
from tests.basicswap.common import ( from tests.basicswap.common import (
BASE_PORT, BASE_PORT,
BASE_RPC_PORT, BASE_RPC_PORT,
@@ -35,6 +32,7 @@ from tests.basicswap.common import (
LTC_BASE_PORT, LTC_BASE_PORT,
LTC_BASE_RPC_PORT, LTC_BASE_RPC_PORT,
PIVX_BASE_PORT, PIVX_BASE_PORT,
BTC_USE_DESCRIPTORS,
) )
from tests.basicswap.extended.test_dcr import ( from tests.basicswap.extended.test_dcr import (
DCR_BASE_PORT, DCR_BASE_PORT,
@@ -49,8 +47,6 @@ from tests.basicswap.extended.test_doge import (
DOGE_BASE_RPC_PORT, DOGE_BASE_RPC_PORT,
) )
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
import basicswap.config as cfg import basicswap.config as cfg
import basicswap.bin.run as runSystem 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["PART_RPC_PORT"] = str(PARTICL_RPC_PORT_BASE)
os.environ["BTC_RPC_PORT"] = str(BITCOIN_RPC_PORT_BASE) os.environ["BTC_RPC_PORT"] = str(BITCOIN_RPC_PORT_BASE)
os.environ["BTC_PORT"] = str(BITCOIN_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["LTC_RPC_PORT"] = str(LITECOIN_RPC_PORT_BASE)
os.environ["DCR_RPC_PORT"] = str(DECRED_RPC_PORT_BASE) os.environ["DCR_RPC_PORT"] = str(DECRED_RPC_PORT_BASE)
os.environ["FIRO_RPC_PORT"] = str(FIRO_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 ( from basicswap.util import (
make_int, make_int,
) )
from basicswap.util.extkey import ExtKeyPair
from basicswap.interface.base import Curves from basicswap.interface.base import Curves
from tests.basicswap.util import ( from tests.basicswap.util import (
read_json_api, read_json_api,
@@ -40,6 +41,7 @@ from tests.basicswap.common import (
wait_for_none_active, wait_for_none_active,
BTC_BASE_RPC_PORT, BTC_BASE_RPC_PORT,
) )
from basicswap.contrib.test_framework.descriptors import descsum_create
from basicswap.contrib.test_framework.messages import ( from basicswap.contrib.test_framework.messages import (
ToHex, ToHex,
FromHex, FromHex,
@@ -58,6 +60,8 @@ from .test_xmr import BaseTest, test_delay_event, callnoderpc
logger = logging.getLogger() logger = logging.getLogger()
test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b"
class TestFunctions(BaseTest): class TestFunctions(BaseTest):
base_rpc_port = None base_rpc_port = None
@@ -1166,7 +1170,6 @@ class BasicSwapTest(TestFunctions):
logging.info("---------- Test {} hdwallet".format(self.test_coin_from.name)) logging.info("---------- Test {} hdwallet".format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from) ci = self.swap_clients[0].ci(self.test_coin_from)
test_seed = "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b"
test_wif = ( test_wif = (
self.swap_clients[0] self.swap_clients[0]
.ci(self.test_coin_from) .ci(self.test_coin_from)
@@ -1178,10 +1181,35 @@ class BasicSwapTest(TestFunctions):
"createwallet", [new_wallet_name, False, True, "", False, False] "createwallet", [new_wallet_name, False, True, "", False, False]
) )
self.callnoderpc("sethdseed", [True, test_wif], wallet=new_wallet_name) 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) 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" 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) self.swap_clients[0].initialiseWallet(Coins.BTC, raise_errors=True)
assert self.swap_clients[0].checkWalletSeed(Coins.BTC) is True assert self.swap_clients[0].checkWalletSeed(Coins.BTC) is True
for i in range(1500): for i in range(1500):
@@ -1561,6 +1589,97 @@ class BasicSwapTest(TestFunctions):
) )
assert len(tx_wallet["blockhash"]) == 64 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): def test_01_0_lock_bad_prevouts(self):
logging.info( logging.info(
"---------- Test {} lock_bad_prevouts".format(self.test_coin_from.name) "---------- 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"] assert "seed is set from the Basicswap mnemonic" in rv["error"]
rv = read_json_api(1800, "getcoinseed", {"coin": "BTC"}) rv = read_json_api(1800, "getcoinseed", {"coin": "BTC"})
assert ( assert rv["seed"] == test_seed
rv["seed"] assert rv["seed_id"] in (
== "8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b" "3da5c0af91879e8ce97d9a843874601c08688078",
"4a231080ec6f4078e543d39cc6dcf0b922c9b16b",
) )
assert rv["seed_id"] == "3da5c0af91879e8ce97d9a843874601c08688078"
assert rv["seed_id"] == rv["expected_seed_id"] assert rv["seed_id"] == rv["expected_seed_id"]
rv = read_json_api( rv = read_json_api(

View File

@@ -83,6 +83,7 @@ from tests.basicswap.common import (
LTC_BASE_PORT, LTC_BASE_PORT,
LTC_BASE_RPC_PORT, LTC_BASE_RPC_PORT,
PREFIX_SECRET_KEY_REGTEST, PREFIX_SECRET_KEY_REGTEST,
BTC_USE_DESCRIPTORS,
) )
from basicswap.db_util import ( from basicswap.db_util import (
remove_expired_data, remove_expired_data,
@@ -172,6 +173,7 @@ def prepare_swapclient_dir(
"datadir": os.path.join(datadir, "btc_" + str(node_id)), "datadir": os.path.join(datadir, "btc_" + str(node_id)),
"bindir": cfg.BITCOIN_BINDIR, "bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True, "use_segwit": True,
"use_descriptors": BTC_USE_DESCRIPTORS,
}, },
}, },
"check_progress_seconds": 2, "check_progress_seconds": 2,
@@ -189,6 +191,9 @@ def prepare_swapclient_dir(
"restrict_unknown_seed_wallets": False, "restrict_unknown_seed_wallets": False,
} }
if BTC_USE_DESCRIPTORS:
settings["chainclients"]["bitcoin"]["watch_wallet_name"] = "bsx_watch"
if Coins.XMR in with_coins: if Coins.XMR in with_coins:
settings["chainclients"]["monero"] = { settings["chainclients"]["monero"] = {
"connection_type": "rpc", "connection_type": "rpc",
@@ -474,25 +479,29 @@ class BaseTest(unittest.TestCase):
if os.path.exists( if os.path.exists(
os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet") os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet")
): ):
try: if BTC_USE_DESCRIPTORS:
callrpc_cli( # How to set blank and disable_private_keys with wallet util?
cfg.BITCOIN_BINDIR, pass
data_dir, else:
"regtest", try:
"-wallet=wallet.dat -legacy create", callrpc_cli(
"bitcoin-wallet", cfg.BITCOIN_BINDIR,
) data_dir,
except Exception as e: "regtest",
logging.warning( "-wallet=wallet.dat -legacy create",
f"bitcoin-wallet create failed {e}, retrying without -legacy" "bitcoin-wallet",
) )
callrpc_cli( except Exception as e:
cfg.BITCOIN_BINDIR, logging.warning(
data_dir, f"bitcoin-wallet create failed {e}, retrying without -legacy"
"regtest", )
"-wallet=wallet.dat create", callrpc_cli(
"bitcoin-wallet", cfg.BITCOIN_BINDIR,
) data_dir,
"regtest",
"-wallet=wallet.dat create",
"bitcoin-wallet",
)
cls.btc_daemons.append( cls.btc_daemons.append(
startDaemon( startDaemon(
@@ -505,9 +514,21 @@ class BaseTest(unittest.TestCase):
"Started %s %d", cfg.BITCOIND, cls.part_daemons[-1].handle.pid "Started %s %d", cfg.BITCOIND, cls.part_daemons[-1].handle.pid
) )
waitForRPC( if BTC_USE_DESCRIPTORS:
make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT), test_delay_event 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: if cls.start_ltc_nodes:
for i in range(NUM_LTC_NODES): for i in range(NUM_LTC_NODES):
@@ -658,6 +679,11 @@ class BaseTest(unittest.TestCase):
xmr_ci.getMainWalletAddress(), 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) t = HttpThread(sc.fp, TEST_HTTP_HOST, TEST_HTTP_PORT + i, False, sc)
cls.http_threads.append(t) cls.http_threads.append(t)
t.start() t.start()
@@ -685,6 +711,7 @@ class BaseTest(unittest.TestCase):
"getnewaddress", "getnewaddress",
["mining_addr", "bech32"], ["mining_addr", "bech32"],
base_rpc_port=BTC_BASE_RPC_PORT, base_rpc_port=BTC_BASE_RPC_PORT,
wallet="wallet.dat",
) )
num_blocks = 400 # Mine enough to activate segwit num_blocks = 400 # Mine enough to activate segwit
logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr) logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr)
@@ -700,6 +727,7 @@ class BaseTest(unittest.TestCase):
"getnewaddress", "getnewaddress",
["initial addr"], ["initial addr"],
base_rpc_port=BTC_BASE_RPC_PORT, base_rpc_port=BTC_BASE_RPC_PORT,
wallet="wallet.dat",
) )
for i in range(5): for i in range(5):
callnoderpc( callnoderpc(
@@ -707,6 +735,7 @@ class BaseTest(unittest.TestCase):
"sendtoaddress", "sendtoaddress",
[btc_addr1, 100], [btc_addr1, 100],
base_rpc_port=BTC_BASE_RPC_PORT, base_rpc_port=BTC_BASE_RPC_PORT,
wallet="wallet.dat",
) )
# Switch addresses so wallet amounts stay constant # Switch addresses so wallet amounts stay constant