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

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