mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 18:38:09 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
64
basicswap/contrib/test_framework/descriptors.py
Normal file
64
basicswap/contrib/test_framework/descriptors.py
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user