# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # needs local bitcoind in PATH import os, time, uuid, socket, shutil, pytest, tempfile, subprocess, signal, base64 from authproxy import AuthServiceProxy, JSONRPCException from helpers import xfp2str from ckcc.protocol import CCProtocolPacker def find_bitcoind(): # search for the binary we need # - should be in the path really easy = shutil.which('bitcoind') if easy: return easy # - default landing spot for MacOS .dmg from bitcoin.org mac_default = '/Applications/Bitcoin-Qt.app/Contents/MacOS/Bitcoin-Qt' if os.path.exists(mac_default): return mac_default raise RuntimeError("Need a binary for bitcoin core. Check path?") # stolen from HWI test suite and slightly modified class Bitcoind: def __init__(self): self.bitcoind_path = find_bitcoind() self.datadir = tempfile.mkdtemp() self.rpc = None self.bitcoind_proc = None self.userpass = None self.supply_wallet = None self.has_bdb = True self.version = None def start(self): def get_free_port(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("", 0)) s.listen(1) port = s.getsockname()[1] s.close() return port self.p2p_port = get_free_port() self.rpc_port = get_free_port() self.bitcoind_proc = subprocess.Popen( [ self.bitcoind_path, # needed for newest master # legacy wallet was deprecated in v29 # and removed completely in v30 "-deprecatedrpc=create_bdb", "-regtest", f"-datadir={self.datadir}", "-noprinttoconsole", "-fallbackfee=0.0002", "-server=1", "-keypool=1", "-listen=0", f"-port={self.p2p_port}", f"-rpcport={self.rpc_port}", ] ) signal.signal(signal.SIGTERM, self.cleanup) # Wait for cookie file to be created cookie_path = os.path.join(self.datadir, "regtest", ".cookie") for i in range(20): if os.path.exists(cookie_path): break time.sleep(0.5) else: RuntimeError("'.cookie' not found. Is bitcoind running?") # Read .cookie file to get user and pass with open(cookie_path) as f: self.userpass = f.readline().lstrip().rstrip() self.rpc_url = f"http://{self.userpass}@127.0.0.1:{self.rpc_port}" self.rpc = AuthServiceProxy(self.rpc_url) # Wait for bitcoind to be ready ready = False while not ready: try: self.rpc.getblockchaininfo() ready = True except JSONRPCException: time.sleep(0.5) pass assert self.rpc.getblockchaininfo()['chain'] == 'regtest' self.version = self.rpc.getnetworkinfo()['version'] assert self.version >= 220000, "we require >= 22.0 of Core" # not descriptors so that we can do dumpwallet try: self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=False) except JSONRPCException as e: assert "BDB wallet creation is deprecated" in str(e) \ or "no longer possible to create a legacy wallet" in str(e) # before v30.0 vs v30.0+ self.has_bdb = False self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=True) # Make sure there are blocks and coins available self.supply_wallet.generatetoaddress(101, self.supply_wallet.getnewaddress()) def get_wallet_rpc(self, wallet): url = self.rpc_url + f"/wallet/{wallet}" return AuthServiceProxy(url) def create_wallet(self, wallet_name: str, disable_private_keys: bool = False, blank: bool = False, passphrase: str = None, avoid_reuse: bool = False, descriptors: bool = True, load_on_startup: bool = False, external_signer: bool = False) -> AuthServiceProxy: """Create wallet and return AuthServiceProxy object to that wallet""" self.rpc.createwallet(wallet_name=wallet_name, disable_private_keys=disable_private_keys, blank=blank, passphrase=passphrase, avoid_reuse=avoid_reuse, descriptors=descriptors, load_on_startup=load_on_startup, external_signer=external_signer) return self.get_wallet_rpc(wallet_name) def cleanup(self, *args, **kwargs): if self.bitcoind_proc is not None and self.bitcoind_proc.poll() is None: self.bitcoind_proc.kill() time.sleep(0.5) shutil.rmtree(self.datadir) def delete_wallet_files(self, pattern=None): wallets_dir = os.path.join(self.datadir, "regtest/wallets") wallet_files = os.listdir(wallets_dir) for wf in wallet_files: abs_path = os.path.join(wallets_dir, wf) if pattern is None: # remove all shutil.rmtree(abs_path) else: if pattern in wf: shutil.rmtree(abs_path) @staticmethod def create(*args, **kwargs): c = Bitcoind(*args, **kwargs) c.start() return c @pytest.fixture def bitcoind(): # JSON-RPC connection to a bitcoind instance # this assumes that you have bitcoind in path somewhere bitcoin_d = Bitcoind.create() yield bitcoin_d os.killpg(os.getpgid(bitcoin_d.bitcoind_proc.pid), signal.SIGTERM) @pytest.fixture def match_key(bitcoind, set_master_key, reset_seed_words): # load simulator w/ existing bip32 master key of testnet instance # bummer: dumpmasterprivkey RPC call was removed! #prv = bitcoind.dumpmasterprivkey() # bummer: dumpwallet RPC call was removed does not work with descriptor wallets try: from tempfile import mktemp fn = mktemp() bitcoind.supply_wallet.dumpwallet(fn) prv = None for ln in open(fn, 'rt').readlines(): if 'extended private masterkey' in ln: assert not prv prv = ln.split(": ", 1)[1].strip() os.unlink(fn) except JSONRPCException as e: assert "Only legacy wallets are supported by this command" in str(e) \ or "Method not found" in str(e) # v30.0 prv_descs = bitcoind.supply_wallet.listdescriptors(True) # True --> show private prv = prv_descs["descriptors"][0]["desc"].replace("pkh(", "").split("/")[0] assert prv.startswith('tprv') xfp = set_master_key(prv) yield xfp @pytest.fixture def finalize_v2_v0_convert(bitcoind): def doit(psbt_obj): # compat wrapper - can be removed after below released # https://github.com/bitcoin/bitcoin/pull/21283 PSBTv2 # convert v2 -> v0 if bitcoind does not support PSBTv2 # to be able to finalize from authproxy import JSONRPCException try: resp = bitcoind.supply_wallet.finalizepsbt(psbt_obj.as_b64_str()) except JSONRPCException as e: assert "Unsupported version number" in e.error["message"] # this version of bitcoind does not support PSBTv2 # convert to v0 - needed for finalize resp = bitcoind.supply_wallet.finalizepsbt( base64.b64encode(psbt_obj.to_v0()).decode() ) return resp return doit @pytest.fixture def bitcoind_wallet(bitcoind): # Use bitcoind to create a temporary wallet file w_name = 'ckcc-test-wallet-%s' % uuid.uuid4() conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True, passphrase=None, avoid_reuse=False, descriptors=not bitcoind.has_bdb) yield conn @pytest.fixture def bitcoind_d_wallet(bitcoind): # Use bitcoind to create a temporary DESCRIPTOR-based wallet file w_name = 'ckcc-test-desc-wallet-%s' % uuid.uuid4() conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True, passphrase=None, avoid_reuse=False, descriptors=True) yield conn @pytest.fixture def bitcoind_d_wallet_w_sk(bitcoind): # Use bitcoind to create a temporary DESCRIPTOR-based wallet file w_name = 'ckcc-test-desc-wallet-w-sk-%s' % uuid.uuid4() conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=False, blank=False, passphrase=None, avoid_reuse=False, descriptors=True) yield conn @pytest.fixture def bitcoind_d_sim_watch(bitcoind): # watch only descriptor wallet simulator w_name = 'ckcc-test-desc-wallet-sim-%s' % uuid.uuid4() conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True, passphrase=None, avoid_reuse=False, descriptors=True) descriptors = [ { "timestamp": "now", "active": True, "desc": "wpkh([0f056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/0/*)#erexmnep", "internal": False }, { "desc": "wpkh([0f056943/84h/1h/0h]tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/1/*)#ghu8xxfe", "active": True, "internal": True, "timestamp": "now" }, { "timestamp": "now", "active": True, "desc": "tr([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1/0/*)#6ghw47ge", "internal": False }, { "desc": "tr([0f056943/86h/1h/0h]tpubDCeEX49avtiXrBTv3JWTtco99Ka499jXdZHBRtm7va2gkMAui11ctZjqNAT9dLVNaEozt2C1kfTM88cnvZCXsWLJN2p4viGvsyGjtKVV7A1/1/*)#tuj0gtcp", "active": True, "internal": True, "timestamp": "now" }, { "timestamp": "now", "active": True, "desc": "pkh([0f056943/44h/1h/0h]tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB/0/*)#fxwk08tc", "internal": False }, { "timestamp": "now", "active": True, "desc": "pkh([0f056943/44h/1h/0h]tpubDCiHGUNYdRRBPNYm7CqeeLwPWfeb2ZT2rPsk4aEW3eUoJM93jbBa7hPpB1T9YKtigmjpxHrB1522kSsTxGm9V6cqKqrp1EDaYaeJZqcirYB/1/*)#cjthjjmq", "internal": True }, { "timestamp": "now", "active": True, "desc": "sh(wpkh([0f056943/49h/1h/0h]tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj/0/*))#weah3vek", "internal": False }, { "timestamp": "now", "active": True, "desc": "sh(wpkh([0f056943/49h/1h/0h]tpubDCDqt7XXvhAYY9HSwrCXB7BXqYM4RXB8WFtKgtTXGa6u3U6EV1NJJRFTcuTRyhSY5Vreg1LP8aPdyiAPQGrDJLikkHoc7VQg6DA9NtUxHtj/1/*))#mcnpfnvf", "internal": True }, ] conn.importdescriptors(descriptors) yield conn @pytest.fixture def bitcoind_d_dev_watch(request, dev, bitcoind, dev_core_import_object): name = "" if dev.is_simulator: settings_set = request.getfixturevalue('settings_set') settings_set("chain", "XRT") name += "sim" assert dev.send_recv(CCProtocolPacker.block_chain()) == "XRT", "needs regtest" xfp = xfp2str(dev.master_fingerprint) name += xfp name += "watch-only" w_name = '%s-%s' % (name, uuid.uuid4()) conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=True, blank=True, passphrase=None, avoid_reuse=False, descriptors=True) conn.importdescriptors(dev_core_import_object) yield conn @pytest.fixture def bitcoind_d_sim_sign(bitcoind): # Use bitcoind to create a clone of simulator wallet with private keys w_name = 'ckcc-test-desc-wallet-sim-%s' % uuid.uuid4() conn = bitcoind.create_wallet(wallet_name=w_name, disable_private_keys=False, blank=True, passphrase=None, avoid_reuse=False, descriptors=True) # below is simulator descriptor wallet descriptors = [ { "timestamp": "now", "active": True, "desc": "wpkh([0f056943/84h/1h/0h]tprv8fRh8AYC5iQitbbtzwVaUUyXVZh3Y7HxVYSbqzf45eao9SMfEc3MexJx4y6pU1WjjxcEiYArEjhRTSy5mqfXzBtSncTYhKfxQWywcfeqxFE/0/*)#mzg0pna0", "internal": False }, { "timestamp": "now", "active": True, "desc": "wpkh([0f056943/84h/1h/0h]tprv8fRh8AYC5iQitbbtzwVaUUyXVZh3Y7HxVYSbqzf45eao9SMfEc3MexJx4y6pU1WjjxcEiYArEjhRTSy5mqfXzBtSncTYhKfxQWywcfeqxFE/1/*)#2kdwuxdh", "internal": True }, { "timestamp": "now", "active": True, "desc": "tr([0f056943/86h/1h/0h]tprv8fxCNe7LnX2rxiS89eqsVD92aJ47ypYd4FgQ9NipWJEHurv95cC2i57yC2mRHnpuHfmgdb17GV9wfSNjswUQXmaY7Qs2Jaa5hEdkxaHy4BK/0/*)#x7dfk9mw", "internal": False }, { "desc": "tr([0f056943/86h/1h/0h]tprv8fxCNe7LnX2rxiS89eqsVD92aJ47ypYd4FgQ9NipWJEHurv95cC2i57yC2mRHnpuHfmgdb17GV9wfSNjswUQXmaY7Qs2Jaa5hEdkxaHy4BK/1/*)#h2ggtstk", "active": True, "internal": True, "timestamp": "now" }, { "timestamp": "now", "active": True, "desc": "pkh([0f056943/44h/1h/0h]tprv8g2F84LJV3jWVuWyDZB4EwHGwe8esEG8H6Gxn4CCdNgQTrtH7CMywCmwzuMGZjz13sQ9rcCZucCm6i2zigkYGSPUvCzDQxGW8RCy7FpPdrg/0/*)#kjnlnm3v", "internal": False }, { "timestamp": "now", "active": True, "desc": "pkh([0f056943/44h/1h/0h]tprv8g2F84LJV3jWVuWyDZB4EwHGwe8esEG8H6Gxn4CCdNgQTrtH7CMywCmwzuMGZjz13sQ9rcCZucCm6i2zigkYGSPUvCzDQxGW8RCy7FpPdrg/1/*)#8xk7wwp5", "internal": True }, { "timestamp": "now", "active": True, "desc": "sh(wpkh([0f056943/49h/1h/0h]tprv8fXojhVHnKUsegFf4CXvmhXRGWq8GBzDvxHYQNRDrJJWCyqTrcYi7vdbSn65CHETVPdw4sxc75v23Ev7o8fCePazRf917CMt1C3mjnKV4Jq/0/*))#0qf5gv2y", "internal": False }, { "timestamp": "now", "active": True, "desc": "sh(wpkh([0f056943/49h/1h/0h]tprv8fXojhVHnKUsegFf4CXvmhXRGWq8GBzDvxHYQNRDrJJWCyqTrcYi7vdbSn65CHETVPdw4sxc75v23Ev7o8fCePazRf917CMt1C3mjnKV4Jq/1/*))#6p8zsnlm", "internal": True }, ] conn.importdescriptors(descriptors) yield conn # EOF