From e9ed334a541ed025b698b1955e6997cb56d985ef Mon Sep 17 00:00:00 2001 From: tecnovert Date: Sun, 30 Mar 2025 00:32:08 +0200 Subject: [PATCH] nmc: Use descriptor wallets by default. --- basicswap/bin/prepare.py | 17 ++++++-- basicswap/chainparams.py | 6 +++ basicswap/interface/btc.py | 62 +++++++++++++++++++++++--- basicswap/interface/nmc.py | 54 ----------------------- tests/basicswap/extended/test_nmc.py | 19 ++++++-- tests/basicswap/test_btc_xmr.py | 65 ++++++++++++++++++++++++++-- 6 files changed, 152 insertions(+), 71 deletions(-) diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index 233e30f..f70f602 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -379,6 +379,12 @@ def getWalletName(coin_params: str, default_name: str, prefix_override=None) -> return wallet_name +def getDescriptorWalletOption(coin_params): + ticker: str = coin_params["ticker"] + default_option: bool = True if ticker in ("NMC",) else False + return toBool(os.getenv(ticker + "_USE_DESCRIPTORS", default_option)) + + def getKnownVersion(coin_name: str) -> str: version, version_tag, _ = known_coins[coin_name] return version + version_tag @@ -1928,11 +1934,15 @@ def initialise_wallets( ], ) if use_descriptors: + watch_wallet_name = coin_settings["watch_wallet_name"] + logger.info( + f'Creating wallet "{watch_wallet_name}" for {getCoinName(c)}.' + ) swap_client.callcoinrpc( c, "createwallet", [ - coin_settings["watch_wallet_name"], + watch_wallet_name, True, True, "", @@ -2596,9 +2606,8 @@ def main(): 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,): + if getDescriptorWalletOption(coin_params): + if coin_id not in (Coins.BTC, Coins.NMC): raise ValueError(f"Descriptor wallet unavailable for {coin_name}") coin_settings["use_descriptors"] = True diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index f4e36d5..efbb58b 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -58,6 +58,8 @@ chainparams = { "bip44": 44, "min_amount": 100000, "max_amount": 10000000 * COIN, + "ext_public_key_prefix": 0x696E82D1, + "ext_secret_key_prefix": 0x8F1DAEB8, }, "testnet": { "rpcport": 51935, @@ -69,6 +71,8 @@ chainparams = { "bip44": 1, "min_amount": 100000, "max_amount": 10000000 * COIN, + "ext_public_key_prefix": 0xE1427800, + "ext_secret_key_prefix": 0x04889478, }, "regtest": { "rpcport": 51936, @@ -80,6 +84,8 @@ chainparams = { "bip44": 1, "min_amount": 100000, "max_amount": 10000000 * COIN, + "ext_public_key_prefix": 0xE1427800, + "ext_secret_key_prefix": 0x04889478, }, }, Coins.BTC: { diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 618ddec..b8656c8 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -1862,20 +1862,70 @@ class BTCInterface(Secp256k1Interface): "Could not find address with enough funds for proof", ) - self._log.debug("sign_for_addr %s", sign_for_addr) + self._log.debug(f"sign_for_addr {sign_for_addr}") + funds_addr: str = sign_for_addr if ( self.using_segwit() ): # TODO: Use isSegwitAddress when scantxoutset can use combo # 'Address does not refer to key' for non p2pkh pkh = self.decodeAddress(sign_for_addr) sign_for_addr = self.pkh_to_address(pkh) - self._log.debug("sign_for_addr converted %s", sign_for_addr) + self._log.debug(f"sign_for_addr converted {sign_for_addr}") - signature = self.rpc_wallet( - "signmessage", - [sign_for_addr, sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()], - ) + if self._use_descriptors: + # https://github.com/bitcoin/bitcoin/issues/10542 + # https://github.com/bitcoin/bitcoin/issues/26046 + priv_keys = self.rpc_wallet( + "listdescriptors", + [ + True, + ], + ) + addr_info = self.rpc_wallet( + "getaddressinfo", + [ + funds_addr, + ], + ) + hdkeypath = addr_info["hdkeypath"] + + sign_for_address_key = None + for descriptor in priv_keys["descriptors"]: + if descriptor["active"] is False or descriptor["internal"] is True: + continue + desc = descriptor["desc"] + assert desc.startswith("wpkh(") + ext_key = desc[5:].split(")")[0].split("/", 1)[0] + ext_key_data = decodeAddress(ext_key)[4:] + ci_part = self._sc.ci(Coins.PART) + ext_key_data_part = ci_part.encode_secret_extkey(ext_key_data) + rv = ci_part.rpc_wallet( + "extkey", ["info", ext_key_data_part, hdkeypath] + ) + extkey_derived = rv["key_info"]["result"] + ext_key_data = decodeAddress(extkey_derived)[4:] + ek = ExtKeyPair() + ek.decode(ext_key_data) + sign_for_address_key = self.encodeKey(ek._key) + break + assert sign_for_address_key is not None + signature = self.rpc( + "signmessagewithprivkey", + [ + sign_for_address_key, + sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(), + ], + ) + del priv_keys + else: + signature = self.rpc_wallet( + "signmessage", + [ + sign_for_addr, + sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(), + ], + ) prove_utxos = [] # TODO: Send specific utxos return (sign_for_addr, signature, prove_utxos) diff --git a/basicswap/interface/nmc.py b/basicswap/interface/nmc.py index 19aa48e..0214857 100644 --- a/basicswap/interface/nmc.py +++ b/basicswap/interface/nmc.py @@ -14,57 +14,3 @@ class NMCInterface(BTCInterface): @staticmethod def coin_type(): return Coins.NMC - - def lockNonSegwitPrevouts(self) -> None: - # For tests - # NMC Seems to ignore utxo locks - unspent = self.rpc_wallet("listunspent") - - to_lock = [] - for u in unspent: - if u.get("spendable", False) is False: - continue - if "desc" in u: - desc = u["desc"] - if self.use_p2shp2wsh(): - if not desc.startswith("sh(wpkh"): - to_lock.append( - { - "txid": u["txid"], - "vout": u["vout"], - "amount": u["amount"], - } - ) - else: - if not desc.startswith("wpkh"): - to_lock.append( - { - "txid": u["txid"], - "vout": u["vout"], - "amount": u["amount"], - } - ) - - if len(to_lock) > 0: - self._log.debug(f"Spending {len(to_lock)} non segwit prevouts") - addr_out = self.rpc_wallet( - "getnewaddress", ["convert non segwit", "bech32"] - ) - prevouts = [] - sum_amount: int = 0 - for utxo in to_lock: - prevouts.append( - { - "txid": utxo["txid"], - "vout": utxo["vout"], - } - ) - sum_amount += self.make_int(utxo["amount"]) - - fee = 100000 * len(prevouts) - funded_tx = self.rpc( - "createrawtransaction", - [prevouts, {addr_out: self.format_amount(sum_amount - fee)}], - ) - signed_tx = self.rpc_wallet("signrawtransactionwithwallet", [funded_tx]) - self.rpc("sendrawtransaction", [signed_tx["hex"]]) diff --git a/tests/basicswap/extended/test_nmc.py b/tests/basicswap/extended/test_nmc.py index ac839e9..aa612ef 100644 --- a/tests/basicswap/extended/test_nmc.py +++ b/tests/basicswap/extended/test_nmc.py @@ -54,7 +54,7 @@ NAMECOIND = os.getenv("NAMECOIND", "namecoind" + cfg.bin_suffix) NAMECOIN_CLI = os.getenv("NAMECOIN_CLI", "namecoin-cli" + cfg.bin_suffix) NAMECOIN_TX = os.getenv("NAMECOIN_TX", "namecoin-tx" + cfg.bin_suffix) -USE_DESCRIPTOR_WALLETS = toBool(os.getenv("USE_DESCRIPTOR_WALLETS", False)) +NMC_USE_DESCRIPTORS = toBool(os.getenv("NMC_USE_DESCRIPTORS", True)) NMC_BASE_PORT = 8136 NMC_BASE_RPC_PORT = 8146 @@ -112,7 +112,7 @@ class TestNMC(BasicSwapTest): base_rpc_port = NMC_BASE_RPC_PORT nmc_addr = None max_fee: int = 200000 - test_fee_rate: int = 10000 # sats/kvB + test_fee_rate: int = 100000 # sats/kvB def mineBlock(self, num_blocks: int = 1) -> None: self.callnoderpc("generatetoaddress", [num_blocks, self.nmc_addr]) @@ -160,8 +160,13 @@ class TestNMC(BasicSwapTest): if len(nmc_rpc("listwallets")) < 1: nmc_rpc( "createwallet", - ["wallet.dat", False, False, "", False, USE_DESCRIPTOR_WALLETS], + ["wallet.dat", False, True, "", False, NMC_USE_DESCRIPTORS], ) + if NMC_USE_DESCRIPTORS: + nmc_rpc( + "createwallet", + ["bsx_watch", True, True, "", False, True], + ) @classmethod def addPIDInfo(cls, sc, i): @@ -180,12 +185,18 @@ class TestNMC(BasicSwapTest): "use_csv": True, "use_segwit": True, "blocks_confirmed": 1, + "use_descriptors": NMC_USE_DESCRIPTORS, } + if NMC_USE_DESCRIPTORS: + settings["chainclients"]["namecoin"]["watch_wallet_name"] = "bsx_watch" @classmethod def prepareExtraCoins(cls): ci0 = cls.swap_clients[0].ci(cls.test_coin) if not cls.restore_instance: + for sc in cls.swap_clients: + ci = sc.ci(cls.test_coin) + ci.initialiseWallet(ci.getNewRandomKey()) cls.nmc_addr = ci0.rpc_wallet("getnewaddress", ["mining_addr", "bech32"]) else: addrs = ci0.rpc_wallet( @@ -214,7 +225,7 @@ class TestNMC(BasicSwapTest): new_wallet_name = random.randbytes(10).hex() self.callnoderpc( "createwallet", - [new_wallet_name, False, False, "", False, USE_DESCRIPTOR_WALLETS], + [new_wallet_name, False, False, "", False, NMC_USE_DESCRIPTORS], ) self.callnoderpc("sethdseed", [True, test_wif], wallet=new_wallet_name) addr = self.callnoderpc( diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index 32d2082..a79c5a8 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -26,6 +26,9 @@ from basicswap.db import ( from basicswap.util import ( make_int, ) +from basicswap.util.address import ( + decodeAddress, +) from basicswap.util.extkey import ExtKeyPair from basicswap.interface.base import Curves from tests.basicswap.util import ( @@ -182,7 +185,7 @@ class TestFunctions(BaseTest): bid0 = read_json_api(1800 + id_offerer, f"bids/{bid_id.hex()}") bid1 = read_json_api(1800 + id_bidder, f"bids/{bid_id.hex()}") - tolerance = 1 + tolerance = 2 assert bid0["ticker_from"] == ci_from.ticker() assert bid1["ticker_from"] == ci_from.ticker() assert bid0["ticker_to"] == ci_to.ticker() @@ -1180,6 +1183,10 @@ class BasicSwapTest(TestFunctions): logging.info("---------- Test {} hdwallet".format(self.test_coin_from.name)) ci = self.swap_clients[0].ci(self.test_coin_from) + if hasattr(ci, "_use_descriptors") and ci._use_descriptors: + logging.warning("Skipping test") + return + test_wif = ( self.swap_clients[0] .ci(self.test_coin_from) @@ -1310,7 +1317,7 @@ class BasicSwapTest(TestFunctions): # Record unspents before createSCLockTx as the used ones will be locked unspents = ci.rpc_wallet("listunspent") - + lockedunspents_before = ci.rpc_wallet("listlockunspent") a = ci.getNewRandomKey() b = ci.getNewRandomKey() @@ -1322,8 +1329,17 @@ class BasicSwapTest(TestFunctions): lock_tx = ci.fundSCLockTx(lock_tx, self.test_fee_rate) lock_tx = ci.signTxWithWallet(lock_tx) + # Check that inputs were locked + lockedunspents = ci.rpc_wallet("listlockunspent") + assert len(lockedunspents) > len(lockedunspents_before) unspents_after = ci.rpc_wallet("listunspent") - assert len(unspents) > len(unspents_after) + for utxo in unspents_after: + for locked_utxo in lockedunspents: + if ( + locked_utxo["txid"] == utxo["txid"] + and locked_utxo["vout"] == utxo["vout"] + ): + raise ValueError("Locked utxo in listunspent") tx_decoded = ci.rpc("decoderawtransaction", [lock_tx.hex()]) txid = tx_decoded["txid"] @@ -1679,6 +1695,49 @@ class BasicSwapTest(TestFunctions): wallet=new_wallet_name, ) + # https://github.com/bitcoin/bitcoin/issues/10542 + # https://github.com/bitcoin/bitcoin/issues/26046 + sign_for_address: str = self.callnoderpc( + "getnewaddress", + [ + "sign address", + ], + wallet=new_wallet_name, + ) + priv_keys = self.callnoderpc("listdescriptors", [True], wallet=new_wallet_name) + addr_info = self.callnoderpc( + "getaddressinfo", [sign_for_address], wallet=new_wallet_name + ) + hdkeypath = addr_info["hdkeypath"] + + sign_for_address_key = None + for descriptor in priv_keys["descriptors"]: + if descriptor["active"] is False or descriptor["internal"] is True: + continue + desc = descriptor["desc"] + assert desc.startswith("wpkh(") + ext_key = desc[5:].split(")")[0].split("/", 1)[0] + ext_key_data = decodeAddress(ext_key)[4:] + ci_part = self.swap_clients[0].ci(Coins.PART) + ext_key_data_part = ci_part.encode_secret_extkey(ext_key_data) + rv = ci_part.rpc_wallet("extkey", ["info", ext_key_data_part, hdkeypath]) + extkey_derived = rv["key_info"]["result"] + ext_key_data = decodeAddress(extkey_derived)[4:] + ek = ExtKeyPair() + ek.decode(ext_key_data) + addr = ci.encodeSegwitAddress(ci.getAddressHashFromKey(ek._key)) + assert addr == sign_for_address + sign_for_address_key = ci.encodeKey(ek._key) + break + assert sign_for_address_key is not None + sign_message: str = "Would be better if dumpprivkey or signmessage worked" + sig = self.callnoderpc( + "signmessagewithprivkey", + [sign_for_address_key, sign_message], + wallet=new_wallet_name, + ) + assert ci.verifyMessage(sign_for_address, sign_message, sig) + self.callnoderpc("unloadwallet", [new_wallet_name]) self.callnoderpc("unloadwallet", [new_watch_wallet_name])