diff --git a/docs/dev-access.md b/docs/dev-access.md index 90f71dbd..c0fe6864 100644 --- a/docs/dev-access.md +++ b/docs/dev-access.md @@ -39,7 +39,7 @@ Yes, external developers can modify COLDCARD and make their own versions! If the red/green light is red, this means some part of flash was changed without the secure checksum inside SE1 being first updated. The upgrade process does this correctly in Mk4, and there is no -point time the checksum is wrong, so there should be no way to see this +point in time the checksum is wrong, so there should be no way to see this screen: ![warning screen](dev-warning.png) diff --git a/docs/installing.md b/docs/installing.md deleted file mode 100644 index e10a648c..00000000 --- a/docs/installing.md +++ /dev/null @@ -1,23 +0,0 @@ - -# Check-out and build - -- clone repo -- do submodule magic in `external` -- make top-level virtual env: `virtualenv -p python3 ENV` -- activate it -- then: - - cd external/ckcc-protocol - pip install -r requirements.txt - pip install --editable . - cd ../.. - pip install -r requirements.txt - pip install -r unix/requirements.txt - -- should give you a command-line program "ckcc" in your path -- should be able to do: - - cd unix - make && ./simulator.py - - diff --git a/shared/chains.py b/shared/chains.py index 4f5e4cc7..15267477 100644 --- a/shared/chains.py +++ b/shared/chains.py @@ -251,12 +251,36 @@ class BitcoinTestnet(BitcoinMain): b44_cointype = 1 +class BitcoinRegtest(BitcoinMain): + ctype = 'XRT' + name = 'Bitcoin Regtest' + menu_name = 'Regtest: BTC' + + slip132 = { + AF_CLASSIC: Slip132Version(0x043587cf, 0x04358394, 't'), + AF_P2WPKH_P2SH: Slip132Version(0x044a5262, 0x044a4e28, 'u'), + AF_P2WPKH: Slip132Version(0x045f1cf6, 0x045f18bc, 'v'), + AF_P2WSH_P2SH: Slip132Version(0x024289ef, 0x024285b5, 'U'), + AF_P2WSH: Slip132Version(0x02575483, 0x02575048, 'V'), + } + + bech32_hrp = 'bcrt' + + b58_addr = bytes([111]) + b58_script = bytes([196]) + b58_privkey = bytes([239]) + + b44_cointype = 1 + + def get_chain(short_name): # lookup object from name: 'BTC' or 'XTN' if short_name == 'BTC': return BitcoinMain elif short_name == 'XTN': return BitcoinTestnet + elif short_name == 'XRT': + return BitcoinRegtest else: raise KeyError(short_name) @@ -271,7 +295,7 @@ def current_chain(): return get_chain(chain) # Overbuilt: will only be testnet and mainchain. -AllChains = [BitcoinMain, BitcoinTestnet] +AllChains = [BitcoinMain, BitcoinTestnet, BitcoinRegtest] def slip32_deserialize(xp): # .. and classify chain and addr-type, as implied by prefix diff --git a/shared/flow.py b/shared/flow.py index fbe04716..56fa0037 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -249,8 +249,8 @@ DangerZoneMenu = [ MenuItem("Set High-Water", f=set_highwater), MenuItem('Wipe HSM Policy', f=wipe_hsm_policy, predicate=hsm_policy_available), MenuItem('Clear OV cache', f=wipe_ovc), - ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3'], - value_map=['BTC', 'XTN'], + ToggleMenuItem('Testnet Mode', 'chain', ['Bitcoin', 'Testnet3', 'Regtest'], + value_map=['BTC', 'XTN', 'XRT'], story="Testnet must only be used by developers because \ correctly- crafted transactions signed on Testnet could be broadcast on Mainnet."), MenuItem('Settings Space', f=show_settings_space), diff --git a/shared/multisig.py b/shared/multisig.py index 5f709234..04bd89ca 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -712,7 +712,8 @@ class MultisigWallet: assert node.privkey() == None # 'no privkeys plz' except ValueError: pass - assert chain.ctype == expect_chain # 'wrong chain' + # HACK but there is no difference extended_keys - just bech32 hrp + assert chain.ctype == expect_chain or expect_chain == "XRT" and chain.ctype == "XTN" # 'wrong chain' depth = node.depth() diff --git a/testing/api.py b/testing/api.py index 8eb46830..ce2cdc10 100644 --- a/testing/api.py +++ b/testing/api.py @@ -6,37 +6,128 @@ # # testnet=1 # server=1 +# rpcservertimeout=2000 # for test_sign.py::test_io_size # -import pytest, os -from bitcoinrpc.authproxy import AuthServiceProxy + +import os +import time +import uuid +import atexit +import socket +import shutil +import pytest +import tempfile +import subprocess +from authproxy import AuthServiceProxy, JSONRPCException from base64 import b64encode, b64decode +from constants import simulator_fixed_words URL = '127.0.0.1:18332/wallet/' -def get_cookie(): - # read local bitcoind cookie .. highly mac-only - AUTHFILE = '~/Library/Application Support/Bitcoin/testnet3/.cookie' - try: - cookie = open(os.path.expanduser(AUTHFILE), 'rt').read().strip() - except FileNotFoundError: - raise pytest.skip('no local bitcoind') +# stolen from HWI test suite and slightly modified +class Bitcoind: + def __init__(self, bitcoind_path, signer="/home/more/PycharmProjects/HWI/venv/lib/python3.8/site-packages/hwi.py"): + self.bitcoind_path = bitcoind_path + self.signer = signer + self.datadir = tempfile.mkdtemp() + self.rpc = None + self.bitcoind_proc = None + self.userpass = None + self.supply_wallet = None - return cookie + def start(self): -@pytest.fixture(scope='function') + 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, + "-regtest", + f"-signer={self.signer}", + f"-datadir={self.datadir}", + "-noprinttoconsole", + "-fallbackfee=0.0002", + "-keypool=1", + f"-port={self.p2p_port}", + f"-rpcport={self.rpc_port}" + ] + ) + + atexit.register(self.cleanup) + + # Wait for cookie file to be created + cookie_path = os.path.join(self.datadir, "regtest", ".cookie") + for i in range(20): + if not os.path.exists(cookie_path): + 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' + assert self.rpc.getnetworkinfo()['version'] >= 220000, "we require >= 22.0 of Core" + # not descriptors so that we can do dumpwallet + self.supply_wallet = self.create_wallet(wallet_name="supply", descriptors=False) + # 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): + if self.bitcoind_proc is not None and self.bitcoind_proc.poll() is None: + self.bitcoind_proc.kill() + shutil.rmtree(self.datadir) + + @staticmethod + def create(*args, **kwargs): + c = Bitcoind(*args, **kwargs) + c.start() + return c + + +@pytest.fixture(scope='session') def bitcoind(): # JSON-RPC connection to a bitcoind instance + # this assumes that you have bitcoind in path somewhere + bitcoin_d = Bitcoind.create("bitcoind") + return bitcoin_d - # see - cookie = get_cookie() - - conn = AuthServiceProxy('http://' + cookie + '@' + URL) - - assert conn.getblockchaininfo()['chain'] == 'test' - assert conn.getnetworkinfo()['version'] >= 220000, "we require >= 22.0 of Core" - - return conn @pytest.fixture def match_key(bitcoind, set_master_key, reset_seed_words): @@ -49,7 +140,7 @@ def match_key(bitcoind, set_master_key, reset_seed_words): print("match_key: doit()") from tempfile import mktemp fn = mktemp() - bitcoind.dumpwallet(fn) + bitcoind.supply_wallet.dumpwallet(fn) prv = None for ln in open(fn, 'rt').readlines(): @@ -68,37 +159,40 @@ def match_key(bitcoind, set_master_key, reset_seed_words): # NOTE: set_master_key does teardown/reset return doit -@pytest.fixture() + +@pytest.fixture def bitcoind_finalizer(bitcoind): # Use bitcoind to finalize a PSBT and get out txn def doit(psbt, extract=True): - rv = bitcoind.finalizepsbt(b64encode(psbt).decode('ascii'), extract) - + rv = bitcoind.rpc.finalizepsbt(b64encode(psbt).decode('ascii'), extract) return b64decode(rv.get('psbt', '')), rv.get('hex'), rv['complete'] return doit -@pytest.fixture() + +@pytest.fixture def bitcoind_analyze(bitcoind): # Use bitcoind to finalize a PSBT and get out txn def doit(psbt): - return bitcoind.analyzepsbt(b64encode(psbt).decode('ascii')) + return bitcoind.rpc.analyzepsbt(b64encode(psbt).decode('ascii')) return doit -@pytest.fixture() + +@pytest.fixture def bitcoind_decode(bitcoind): # Use bitcoind to finalize a PSBT and get out txn def doit(psbt): - return bitcoind.decodepsbt(b64encode(psbt).decode('ascii')) + return bitcoind.rpc.decodepsbt(b64encode(psbt).decode('ascii')) return doit -@pytest.fixture() + +@pytest.fixture def explora(): def doit(*parts): import urllib.request @@ -110,66 +204,82 @@ def explora(): return doit -@pytest.fixture(scope='function') +@pytest.fixture def bitcoind_wallet(bitcoind): - # Use bitcoind to create a temporary wallet file, and then do cleanup after - # - wallet will not have any keys, and is watch-only - import os, shutil + # 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=False) + return conn - fname = '/tmp/ckcc-test-wallet-%d' % os.getpid() - disable_private_keys = True - blank = True - w = bitcoind.createwallet(fname, disable_private_keys, blank) - - assert w['name'] == fname - - # give them an object they can do API calls w/ rpcwallet filled-in - cookie = get_cookie() - url = 'http://' + cookie + '@' + URL + '/wallet/' + fname.replace('/', '%2f') - #print(url) - conn = AuthServiceProxy(url) - assert conn.getblockchaininfo()['chain'] == 'test' - - yield conn - - # cleanup - bitcoind.unloadwallet(fname) - assert fname.startswith('/tmp/ckcc-test-wallet') - shutil.rmtree(fname) - -@pytest.fixture(scope='function') +@pytest.fixture def bitcoind_d_wallet(bitcoind): - # Use bitcoind to create a temporary DESCRIPTOR-based wallet file, and then do cleanup after - # - wallet will not have any keys until a descriptor is added, and is not just watch-only - import os, shutil - - fname = '/tmp/ckcc-test-desc-wallet-%d' % os.getpid() - - disable_private_keys = True - blank = True - password = None - avoid_reuse = False - descriptors = True - w = bitcoind.createwallet(fname, disable_private_keys, blank, - password, avoid_reuse, descriptors) - - assert w['name'] == fname - - # give them an object they can do API calls w/ rpcwallet filled-in - cookie = get_cookie() - url = 'http://' + cookie + '@' + URL + '/wallet/' + fname.replace('/', '%2f') - #print(url) - conn = AuthServiceProxy(url) - assert conn.getblockchaininfo()['chain'] == 'test' - - yield conn - - # cleanup - bitcoind.unloadwallet(fname) - assert fname.startswith('/tmp/ckcc-test-desc-wallet') - shutil.rmtree(fname) + # 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) + return 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) + return conn + + +@pytest.fixture +def bitcoind_d_sim(bitcoind): + # Use bitcoind to create a clone of simulator wallet + 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", + "label": "Coldcard 0f056943", + "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", + "label": "Coldcard 0f056943", + "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", + "label": "Coldcard 0f056943", + "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) + return conn # EOF diff --git a/testing/authproxy.py b/testing/authproxy.py new file mode 100644 index 00000000..4ba6ac1d --- /dev/null +++ b/testing/authproxy.py @@ -0,0 +1,201 @@ +# Copyright (c) 2011 Jeff Garzik +# +# Previous copyright, from python-jsonrpc/jsonrpc/proxy.py: +# +# Copyright (c) 2007 Jan-Klaas Kollhof +# +# This file is part of jsonrpc. +# +# jsonrpc is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this software; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +"""HTTP proxy for opening RPC connection to bitcoind. + +AuthServiceProxy has the following improvements over python-jsonrpc's +ServiceProxy class: + +- HTTP connections persist for the life of the AuthServiceProxy object + (if server supports HTTP/1.1) +- sends protocol 'version', per JSON-RPC 1.1 +- sends proper, incrementing 'id' +- sends Basic HTTP authentication headers +- parses all JSON numbers that look like floats as Decimal +- uses standard Python json lib +""" + +import base64 +import decimal +from http import HTTPStatus +import http.client +import json +import logging +import os +import socket +import time +import urllib.parse + +HTTP_TIMEOUT = 30 +USER_AGENT = "AuthServiceProxy/0.1" + +log = logging.getLogger("BitcoinRPC") + +class JSONRPCException(Exception): + def __init__(self, rpc_error, http_status=None): + try: + errmsg = '%(message)s (%(code)i)' % rpc_error + except (KeyError, TypeError): + errmsg = '' + super().__init__(errmsg) + self.error = rpc_error + self.http_status = http_status + + +def EncodeDecimal(o): + if isinstance(o, decimal.Decimal): + return str(o) + raise TypeError(repr(o) + " is not JSON serializable") + +class AuthServiceProxy(): + __id_count = 0 + + # ensure_ascii: escape unicode as \uXXXX, passed to json.dumps + def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True): + self.__service_url = service_url + self._service_name = service_name + self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests + self.__url = urllib.parse.urlparse(service_url) + user = None if self.__url.username is None else self.__url.username.encode('utf8') + passwd = None if self.__url.password is None else self.__url.password.encode('utf8') + authpair = user + b':' + passwd + self.__auth_header = b'Basic ' + base64.b64encode(authpair) + self.timeout = timeout + self._set_conn(connection) + + def __getattr__(self, name): + if name.startswith('__') and name.endswith('__'): + # Python internal stuff + raise AttributeError + if self._service_name is not None: + name = "%s.%s" % (self._service_name, name) + return AuthServiceProxy(self.__service_url, name, connection=self.__conn) + + def _request(self, method, path, postdata): + ''' + Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout). + This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5. + ''' + headers = {'Host': self.__url.hostname, + 'User-Agent': USER_AGENT, + 'Authorization': self.__auth_header, + 'Content-type': 'application/json'} + if os.name == 'nt': + # Windows somehow does not like to re-use connections + # TODO: Find out why the connection would disconnect occasionally and make it reusable on Windows + self._set_conn() + try: + self.__conn.request(method, path, postdata, headers) + return self._get_response() + except http.client.BadStatusLine as e: + if e.line == "''": # if connection was closed, try again + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + else: + raise + except (BrokenPipeError, ConnectionResetError): + # Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset + # ConnectionResetError happens on FreeBSD with Python 3.4 + self.__conn.close() + self.__conn.request(method, path, postdata, headers) + return self._get_response() + + def get_request(self, *args, **argsn): + AuthServiceProxy.__id_count += 1 + + log.debug("-{}-> {} {}".format( + AuthServiceProxy.__id_count, + self._service_name, + json.dumps(args or argsn, default=EncodeDecimal, ensure_ascii=self.ensure_ascii), + )) + if args and argsn: + raise ValueError('Cannot handle both named and positional arguments') + return {'version': '1.1', + 'method': self._service_name, + 'params': args or argsn, + 'id': AuthServiceProxy.__id_count} + + def __call__(self, *args, **argsn): + postdata = json.dumps(self.get_request(*args, **argsn), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) + response, status = self._request('POST', self.__url.path, postdata.encode('utf-8')) + if response['error'] is not None: + raise JSONRPCException(response['error'], status) + elif 'result' not in response: + raise JSONRPCException({ + 'code': -343, 'message': 'missing JSON-RPC result'}, status) + elif status != HTTPStatus.OK: + raise JSONRPCException({ + 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) + else: + return response['result'] + + def batch(self, rpc_call_list): + postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii) + log.debug("--> " + postdata) + response, status = self._request('POST', self.__url.path, postdata.encode('utf-8')) + if status != HTTPStatus.OK: + raise JSONRPCException({ + 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) + return response + + def _get_response(self): + req_start_time = time.time() + try: + http_response = self.__conn.getresponse() + except socket.timeout: + raise JSONRPCException({ + 'code': -344, + 'message': '%r RPC took longer than %f seconds. Consider ' + 'using larger timeout for calls that take ' + 'longer to return.' % (self._service_name, + self.__conn.timeout)}) + if http_response is None: + raise JSONRPCException({ + 'code': -342, 'message': 'missing HTTP response from server'}) + + content_type = http_response.getheader('Content-Type') + if content_type != 'application/json': + raise JSONRPCException( + {'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}, + http_response.status) + + responsedata = http_response.read().decode('utf8') + response = json.loads(responsedata, parse_float=decimal.Decimal) + elapsed = time.time() - req_start_time + if "error" in response and response["error"] is None: + log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii))) + else: + log.debug("<-- [%.6f] %s" % (elapsed, responsedata)) + return response, http_response.status + + def __truediv__(self, relative_uri): + return AuthServiceProxy("{}/{}".format(self.__service_url, relative_uri), self._service_name, connection=self.__conn) + + def _set_conn(self, connection=None): + port = 80 if self.__url.port is None else self.__url.port + if connection: + self.__conn = connection + self.timeout = connection.timeout + elif self.__url.scheme == 'https': + self.__conn = http.client.HTTPSConnection(self.__url.hostname, port, timeout=self.timeout) + else: + self.__conn = http.client.HTTPConnection(self.__url.hostname, port, timeout=self.timeout) diff --git a/testing/conftest.py b/testing/conftest.py index 415a8960..b5f67596 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,11 +1,10 @@ # (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # -import pytest, glob, time, sys, random, re, ndef -from pprint import pprint -from ckcc.protocol import CCProtocolPacker, CCProtoError +import pytest, time, sys, random, re, ndef +from ckcc.protocol import CCProtocolPacker from helpers import B2A, U2SAT, prandom from api import bitcoind, match_key, bitcoind_finalizer, bitcoind_analyze, bitcoind_decode, explora -from api import bitcoind_wallet, bitcoind_d_wallet +from api import bitcoind_wallet, bitcoind_d_wallet, bitcoind_d_wallet_w_sk, bitcoind_d_sim from binascii import b2a_hex, a2b_hex from constants import * @@ -257,7 +256,7 @@ def addr_vs_path(master_xpub): hrp, data, enc = bech32_decode(given_addr) assert enc == Encoding.BECH32 decoded = convertbits(data[1:], 5, 8, False) - assert hrp in {'tb', 'bc' } + assert hrp in {'tb', 'bc' , 'bcrt'} assert bytes(decoded[-20:]) == pkh else: assert addr_fmt == AF_P2WPKH_P2SH @@ -276,7 +275,7 @@ def addr_vs_path(master_xpub): elif addr_fmt == AF_P2WSH: hrp, data, enc = bech32_decode(given_addr) assert enc == Encoding.BECH32 - assert hrp in {'tb', 'bc' } + assert hrp in {'tb', 'bc' , 'bcrt'} decoded = convertbits(data[1:], 5, 8, False) assert bytes(decoded[-32:]) == sha256(script).digest() @@ -669,6 +668,15 @@ def use_mainnet(settings_set): yield doit settings_set('chain', 'XTN') + +@pytest.fixture(scope="function") +def use_regtest(settings_set): + def doit(): + settings_set('chain', 'XRT') + yield doit + settings_set('chain', 'XTN') + + @pytest.fixture(scope="function") def set_seed_words(sim_exec, sim_execfile, simulator, reset_seed_words): # load simulator w/ a specific bip32 master key @@ -870,7 +878,7 @@ def decode_with_bitcoind(bitcoind): # verify our understanding of a TXN (and esp its outputs) matches # the same values as what bitcoind generates try: - return bitcoind.decoderawtransaction(B2A(raw_txn)) + return bitcoind.rpc.decoderawtransaction(B2A(raw_txn)) except ConnectionResetError: # bitcoind sleeps on us sometimes, give it another chance. return bitcoind.decoderawtransaction(B2A(raw_txn)) @@ -893,17 +901,17 @@ def decode_psbt_with_bitcoind(bitcoind): return doit @pytest.fixture() -def check_against_bitcoind(bitcoind, sim_exec, sim_execfile): +def check_against_bitcoind(bitcoind, use_regtest, sim_exec, sim_execfile): def doit(hex_txn, fee, num_warn=0, change_outs=None, dests=[]): # verify our understanding of a TXN (and esp its outputs) matches # the same values as what bitcoind generates try: - decode = bitcoind.decoderawtransaction(hex_txn) + decode = bitcoind.rpc.decoderawtransaction(hex_txn) except ConnectionResetError: # bitcoind sleeps on us sometimes, give it another chance. - decode = bitcoind.decoderawtransaction(hex_txn) + decode = bitcoind.rpc.decoderawtransaction(hex_txn) #print("Bitcoin code says:", end=''); pprint(decode) diff --git a/testing/constants.py b/testing/constants.py index 9eef9484..a1cddcf3 100644 --- a/testing/constants.py +++ b/testing/constants.py @@ -14,7 +14,7 @@ simulator_fixed_xfp = 0x4369050f simulator_serial_number = 'F1F1F1F1F1F1' -from ckcc_protocol.constants import AF_P2WSH, AFC_SCRIPT, AF_P2SH, AF_P2WSH_P2SH +from ckcc_protocol.constants import AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH unmap_addr_fmt = { 'p2sh': AF_P2SH, diff --git a/testing/devtest/unit_multisig.py b/testing/devtest/unit_multisig.py index d73886b8..8270aa0b 100644 --- a/testing/devtest/unit_multisig.py +++ b/testing/devtest/unit_multisig.py @@ -2,7 +2,7 @@ # # unit test for address decoding for multisig from h import a2b_hex, b2a_hex -from chains import BitcoinMain, BitcoinTestnet +from chains import BitcoinMain, BitcoinTestnet, BitcoinRegtest from multisig import disassemble_multisig from public_constants import AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH from public_constants import AFC_PUBKEY, AFC_SEGWIT, AFC_BECH32, AFC_SCRIPT, AFC_WRAPPED @@ -27,10 +27,15 @@ if 1: addr = BitcoinMain.p2sh_address(AF_P2SH, script) assert addr[0] == '3' assert addr == '3Kt6KxjirrFS7GexJiXLLhmuaMzSbjp275' + addr = BitcoinTestnet.p2sh_address(AF_P2SH, script) assert addr[0] == '2' assert addr == '2NBSJPhfkUJknK4HVyr9CxemAniCcRfhqp4' + addr = BitcoinRegtest.p2sh_address(AF_P2SH, script) + assert addr[0] == '2' + assert addr == '2NBSJPhfkUJknK4HVyr9CxemAniCcRfhqp4' + addr = BitcoinMain.p2sh_address(AF_P2WSH, script) assert addr[0:4] == 'bc1q', addr assert len(addr) >= 62 @@ -41,6 +46,12 @@ if 1: assert len(addr) >= 62 assert addr == 'tb1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkq7r26gv' + addr = BitcoinRegtest.p2sh_address(AF_P2WSH, script) + print(addr) + assert addr[0:6] == 'bcrt1q', addr + assert len(addr) >= 64 + assert addr == 'bcrt1qnjw7wy4e9tf4kkqaf43n2cyjwug0ystugum08c5j5hwhfncc4mkqn6quak' + if 1: from utils import xfp2str, str2xfp diff --git a/testing/helpers.py b/testing/helpers.py index e65cd528..5240d0cf 100644 --- a/testing/helpers.py +++ b/testing/helpers.py @@ -118,7 +118,7 @@ def parse_change_back(story): lines = story.split('\n') s = lines.index('Change back:') assert s > 3 - assert 'XTN' in lines[s+1] or 'BTC' in lines[s+1] + assert 'XTN' in lines[s+1] or 'XRT' in lines[s+1] or 'BTC' in lines[s+1] val = Decimal(lines[s+1].split()[0]) assert 'address' in lines[s+2] addrs = [] diff --git a/testing/psbt.py b/testing/psbt.py index c185dd6e..fa5de250 100644 --- a/testing/psbt.py +++ b/testing/psbt.py @@ -230,7 +230,7 @@ class BasicPSBT: raw = a2b_hex(raw.strip()) if raw[0:6] == b'cHNidP': raw = b64decode(raw) - assert raw[0:5] == b'psbt\xff', "bad magic" + assert raw[0:5] == b'psbt\xff', "bad magic {}".format(raw[0:5]) with io.BytesIO(raw[5:]) as fd: diff --git a/testing/requirements.txt b/testing/requirements.txt index 46dbd93c..fc7c5710 100644 --- a/testing/requirements.txt +++ b/testing/requirements.txt @@ -3,7 +3,6 @@ # for testing (only) pytest==6.2.5 pycoin==0.80 -python-bitcoinrpc>=1.0 pyserial mnemonic==0.18 onetimepass==1.0.1 diff --git a/testing/test_addr.py b/testing/test_addr.py index baf2b6a6..882d0e4d 100644 --- a/testing/test_addr.py +++ b/testing/test_addr.py @@ -56,25 +56,19 @@ def test_show_addr_displayed(dev, need_keypress, addr_vs_path, path, addr_fmt, c assert qr == addr or qr == addr.upper() @pytest.mark.bitcoind -@pytest.mark.parametrize('example_addr', [ - '2N2VBntgcoY4wN7H6VfrhH8an1BwieRMZCF', '2N551pf65tPS7VthC1rvwFDbLA1EUDYkTg9']) -def test_addr_vs_bitcoind(bitcoind, match_key, need_keypress, example_addr, dev): +def test_addr_vs_bitcoind(use_regtest, match_key, need_keypress, dev, bitcoind_d_sim): # check our p2wpkh wrapped in p2sh is right - - # PROBLEM: your bitcoind probably needs same transaction history as mine, so it knows - # about this address and its contents/key path. + use_regtest() + for i in range(5): + core_addr = bitcoind_d_sim.getnewaddress(f"{i}-addr", "p2sh-segwit") + assert core_addr[0] == '2' + resp = bitcoind_d_sim.getaddressinfo(core_addr) + assert resp['embedded']['iswitness'] == True + assert resp['isscript'] == True + path = resp['hdkeypath'] - assert example_addr[0] == '2' - resp = bitcoind.getaddressinfo(example_addr) - - assert resp['embedded']['iswitness'] == True - assert resp['isscript'] == True - path = resp['hdkeypath'] - - addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2WPKH_P2SH), timeout=None) - need_keypress('y') - - assert addr == example_addr - + addr = dev.send_recv(CCProtocolPacker.show_address(path, AF_P2WPKH_P2SH), timeout=None) + need_keypress('y') + assert addr == core_addr # EOF diff --git a/testing/test_export.py b/testing/test_export.py index 54799d2b..928da7d2 100644 --- a/testing/test_export.py +++ b/testing/test_export.py @@ -17,11 +17,11 @@ from ckcc_protocol.constants import AF_CLASSIC, AF_P2WPKH, AF_P2WSH_P2SH from pprint import pprint @pytest.mark.bitcoind -@pytest.mark.parametrize('acct_num', [ None, '0', '99', '123']) -def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, bitcoind_wallet, bitcoind_d_wallet, enter_number): +@pytest.mark.parametrize('acct_num', [None, '0', '99', '123']) +def test_export_core(dev, use_regtest, acct_num, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, bitcoind_wallet, bitcoind_d_wallet, enter_number): # test UX and operation of the 'bitcoin core' wallet export from pycoin.contrib.segwit_addr import encode as sw_encode - + use_regtest() goto_home() pick_menu_item('Advanced/Tools') pick_menu_item('File Management') @@ -71,10 +71,10 @@ def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_sto elif '=>' in ln: path, addr = ln.strip().split(' => ', 1) assert path.startswith(f"m/84'/1'/{acct_num}'/0") - assert addr.startswith('tb1q') + assert addr.startswith('bcrt1q') # TODO here we should differentiate if testnet or smthg sk = BIP32Node.from_wallet_key(simulator_fixed_xprv).subkey_for_path(path[2:]) h20 = sk.hash160() - assert addr == sw_encode(addr[0:2], 0, h20) + assert addr == sw_encode(addr[0:4], 0, h20) # TODO here we should differentiate if testnet or smthg addrs.append(addr) assert len(addrs) == 3 @@ -147,9 +147,9 @@ def test_export_core(dev, acct_num, cap_menu, pick_menu_item, goto_home, cap_sto assert x['address'] == addrs[-1] assert x['iswatchonly'] == False assert x['iswitness'] == True - assert x['ismine'] == True - assert x['solvable'] == True - assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower() + # assert x['ismine'] == True # TODO we have imported pubkeys - it has no idea if it is ours or solvable + # assert x['solvable'] == True + # assert x['hdmasterfingerprint'] == xfp2str(dev.master_fingerprint).lower() #assert x['hdkeypath'] == f"m/84'/1'/{acct_num}'/0/%d" % (len(addrs)-1) @pytest.mark.parametrize('use_nfc', [False, True]) diff --git a/testing/test_multisig.py b/testing/test_multisig.py index 8250d54f..314eb0ac 100644 --- a/testing/test_multisig.py +++ b/testing/test_multisig.py @@ -6,6 +6,7 @@ # # py.test test_multisig.py -m ms_danger --ms-danger # +import base64 import time, pytest, os, random, json, shutil, pdb from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput, PSBT_IN_REDEEM_SCRIPT from ckcc.protocol import CCProtocolPacker, CCProtoError, MAX_TXN_LEN, CCUserRefused @@ -71,10 +72,10 @@ def bitcoind_p2sh(bitcoind): }[fmt] try: - rv = bitcoind.createmultisig(M, [B2A(i) for i in pubkeys], fmt) + rv = bitcoind.rpc.createmultisig(M, [B2A(i) for i in pubkeys], fmt) except ConnectionResetError: # bitcoind sleeps on us sometimes, give it another chance. - rv = bitcoind.createmultisig(M, [B2A(i) for i in pubkeys], fmt) + rv = bitcoind.rpc.createmultisig(M, [B2A(i) for i in pubkeys], fmt) return rv['address'], rv['redeemScript'] @@ -410,8 +411,8 @@ def test_ms_show_addr(dev, cap_story, need_keypress, addr_vs_path, bitcoind_p2sh @pytest.mark.bitcoind @pytest.mark.parametrize('m_of_n', [(1,3), (2,3), (3,3), (3,6), (10, 15), (15,15)]) @pytest.mark.parametrize('addr_fmt', ['p2sh-p2wsh', 'p2sh', 'p2wsh' ]) -def test_import_ranges(m_of_n, addr_fmt, clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr): - +def test_import_ranges(m_of_n, use_regtest, addr_fmt, clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr): + use_regtest() M, N = m_of_n keys = import_ms_wallet(M, N, addr_fmt, accept=1) @@ -428,7 +429,7 @@ def test_import_ranges(m_of_n, addr_fmt, clear_ms, import_ms_wallet, need_keypre @pytest.mark.bitcoind @pytest.mark.ms_danger -def test_violate_bip67(clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr, has_ms_checks): +def test_violate_bip67(clear_ms, use_regtest, import_ms_wallet, need_keypress, test_ms_show_addr, has_ms_checks): # detect when pubkeys are not in order in the redeem script M, N = 1, 15 @@ -446,7 +447,7 @@ def test_violate_bip67(clear_ms, import_ms_wallet, need_keypress, test_ms_show_a @pytest.mark.bitcoind @pytest.mark.parametrize('which_pubkey', [0, 1, 14]) -def test_bad_pubkey(has_ms_checks, clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr, which_pubkey): +def test_bad_pubkey(has_ms_checks, use_regtest, clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr, which_pubkey): # give incorrect pubkey inside redeem script M, N = 1, 15 keys = import_ms_wallet(M, N, accept=1) @@ -467,7 +468,7 @@ def test_bad_pubkey(has_ms_checks, clear_ms, import_ms_wallet, need_keypress, te @pytest.mark.bitcoind @pytest.mark.parametrize('addr_fmt', ['p2sh-p2wsh', 'p2sh', 'p2wsh' ]) -def test_zero_depth(clear_ms, addr_fmt, import_ms_wallet, need_keypress, test_ms_show_addr, make_multisig): +def test_zero_depth(clear_ms, use_regtest, addr_fmt, import_ms_wallet, need_keypress, test_ms_show_addr, make_multisig): # test having a co-signer with "m" only key ... ie. depth=0 M, N = 1, 2 @@ -493,7 +494,7 @@ def test_zero_depth(clear_ms, addr_fmt, import_ms_wallet, need_keypress, test_ms @pytest.mark.parametrize('mode', ['wrong-xfp', 'long-path', 'short-path', 'zero-path']) @pytest.mark.ms_danger @pytest.mark.bitcoind -def test_bad_xfp(mode, clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr, has_ms_checks, request): +def test_bad_xfp(mode, clear_ms, use_regtest, import_ms_wallet, need_keypress, test_ms_show_addr, has_ms_checks, request): # give incorrect xfp+path args during show_address if has_ms_checks and (mode in {'zero-path', 'wrong-xfp'}): @@ -541,7 +542,7 @@ def test_bad_xfp(mode, clear_ms, import_ms_wallet, need_keypress, test_ms_show_a "m/1/2/3/4/5/6/7/8/9/10/11/12/13", # assuming MAX_PATH_DEPTH==12 ]) @pytest.mark.bitcoind -def test_bad_common_prefix(cpp, clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr): +def test_bad_common_prefix(cpp, use_regtest, clear_ms, import_ms_wallet, need_keypress, test_ms_show_addr): # give some incorrect path values as the common prefix derivation M, N = 1, 15 @@ -942,7 +943,7 @@ def test_import_dup_diff_xpub(N, clear_ms, make_multisig, offer_ms_import, need_ @pytest.mark.bitcoind @pytest.mark.parametrize('m_of_n', [(2,2), (2,3), (15,15)]) @pytest.mark.parametrize('addr_fmt', ['p2sh-p2wsh', 'p2sh', 'p2wsh' ]) -def test_import_dup_xfp_fails(m_of_n, addr_fmt, clear_ms, make_multisig, import_ms_wallet, need_keypress, test_ms_show_addr): +def test_import_dup_xfp_fails(m_of_n, use_regtest, addr_fmt, clear_ms, make_multisig, import_ms_wallet, need_keypress, test_ms_show_addr): M, N = m_of_n @@ -1222,7 +1223,7 @@ def test_ms_sign_simple(N, num_ins, dev, addr_fmt, clear_ms, incl_xpubs, import_ @pytest.mark.parametrize('M', [ 2, 4, 1]) @pytest.mark.parametrize('segwit', [True, False]) @pytest.mark.parametrize('incl_xpubs', [ True, False ]) -def test_ms_sign_myself(M, make_myself_wallet, segwit, num_ins, dev, clear_ms, +def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev, clear_ms, fake_ms_txn, try_sign, bitcoind_finalizer, incl_xpubs, bitcoind_analyze, bitcoind_decode): # IMPORTANT: wont work if you start simulator with --ms flag. Use no args @@ -1231,6 +1232,7 @@ def test_ms_sign_myself(M, make_myself_wallet, segwit, num_ins, dev, clear_ms, num_outs = len(all_out_styles) clear_ms() + use_regtest() # create a wallet, with 3 bip39 pw's keys, select_wallet = make_myself_wallet(M, do_import=(not incl_xpubs)) @@ -1240,14 +1242,14 @@ def test_ms_sign_myself(M, make_myself_wallet, segwit, num_ins, dev, clear_ms, psbt = fake_ms_txn(num_ins, num_outs, M, keys, segwit_in=segwit, incl_xpubs=incl_xpubs, outstyles=all_out_styles, change_outputs=list(range(1,num_outs))) - open(f'debug/myself-before.psbt', 'wb').write(psbt) + open(f'debug/myself-before.psbt', 'w').write(base64.b64encode(psbt).decode()) for idx in range(M): select_wallet(idx) _, updated = try_sign(psbt, accept_ms_import=(incl_xpubs and (idx==0))) - open(f'debug/myself-after.psbt', 'wb').write(updated) + open(f'debug/myself-after.psbt', 'w').write(base64.b64encode(updated).decode()) assert updated != psbt - aft = BasicPSBT().parse(updated) + aft = BasicPSBT().parse(updated) # TODO something is off here xpub is longer than 79 - core returs error # check all inputs gained a signature assert all(len(i.part_sigs)==(idx+1) for i in aft.inputs) @@ -1255,15 +1257,14 @@ def test_ms_sign_myself(M, make_myself_wallet, segwit, num_ins, dev, clear_ms, psbt = updated # should be fully signed now. - anal = bitcoind_analyze(aft.as_bytes()) - + anal = bitcoind_analyze(psbt) try: assert not any(inp.get('missing') for inp in anal['inputs']), "missing sigs: %r" % anal assert all(inp['next'] in {'finalizer','updater'} for inp in anal['inputs']), "other issue: %r" % anal except: # XXX seems to be a bug in analyzepsbt function ... not fully studied pprint(anal, stream=open('debug/analyzed.txt', 'wt')) - decode = bitcoind_decode(aft.as_bytes()) + decode = bitcoind_decode(psbt) pprint(decode, stream=open('debug/decoded.txt', 'wt')) if M==N or segwit: @@ -1273,7 +1274,7 @@ def test_ms_sign_myself(M, make_myself_wallet, segwit, num_ins, dev, clear_ms, print("ignoring bug in bitcoind") if 0: - # why doesn't this work? + # why doesn't this work? # TODO produced PSBT is invalid, cannot finalize (both core and us) extracted_psbt, txn, is_complete = bitcoind_finalizer(aft.as_bytes(), extract=True) ex = BasicPSBT().parse(extracted_psbt) @@ -1447,7 +1448,7 @@ def test_make_airgapped(addr_fmt, acct_num, goto_home, cap_story, pick_menu_item @pytest.mark.unfinalized @pytest.mark.bitcoind @pytest.mark.parametrize('addr_style', ["legacy", "p2sh-segwit", "bech32"]) -def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, try_sign, need_keypress, addr_style): +def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, try_sign, need_keypress, addr_style, use_regtest): # Make a P2SH wallet with local bitcoind as a co-signer (and simulator) # - send an receive various # - following text of @@ -1455,8 +1456,7 @@ def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, # - before starting this test, have some funds already deposited to bitcoind testnet wallet from pycoin.encoding import sec_to_public_pair from binascii import a2b_hex - import re - + use_regtest() if addr_style == 'legacy': addr_fmt = AF_P2SH elif addr_style == 'p2sh-segwit': @@ -1464,13 +1464,10 @@ def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, elif addr_style == 'bech32': addr_fmt = AF_P2WSH - try: - addr, = bitcoind.getaddressesbylabel("sim-cosign").keys() - except: - addr = bitcoind.getnewaddress("sim-cosign") - info = bitcoind.getaddressinfo(addr) - #pprint(info) + addr = bitcoind.supply_wallet.getnewaddress("sim-cosign") + + info = bitcoind.supply_wallet.getaddressinfo(addr) assert info['address'] == addr bc_xfp = swab32(int(info['hdmasterfingerprint'], 16)) @@ -1500,7 +1497,7 @@ def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, # NOTE: bitcoind doesn't seem to implement pubkey sorting. We have to do it. - resp = bitcoind.addmultisigaddress(M, list(sorted([cc_pubkey, bc_pubkey])), + resp = bitcoind.supply_wallet.addmultisigaddress(M, list(sorted([cc_pubkey, bc_pubkey])), 'shared-addr-'+addr_style, addr_style) ms_addr = resp['address'] bc_redeem = a2b_hex(resp['redeemScript']) @@ -1529,42 +1526,12 @@ def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, '2N1hZJ5mazTX524GQTPKkCT4UFZn5Fqwdz6', 'tb1qpcv2rkc003p5v8lrglrr6lhz2jg8g4qa9vgtrgkt0p5rteae5xtqn6njw9') - # Need some UTXO to sign - # - # - but bitcoind can't give me that (using listunspent) because it's only a watched addr?? - # - did_fund = False - while 1: - rr = explora('address', ms_addr, 'utxo') - pprint(rr) - - avail = [] - amt = 0 - for i in rr: - txn = i['txid'] - vout = i['vout'] - avail.append( (txn, vout) ) - amt += i['value'] - - # just use first UTXO available; save other for later tests - break - - else: - # doesn't need to confirm, but does need to reach public testnet/blockstream - assert not amt and not avail - - if not did_fund: - print(f"Sending some XTN to {ms_addr} (wait)") - bitcoind.sendtoaddress(ms_addr, 0.0001, 'fund testing') - did_fund = True - else: - print(f"Still waiting ...") - - time.sleep(2) - - if amt: break - - ret_addr = bitcoind.getrawchangeaddress() + # fund multisig address + bitcoind.supply_wallet.importaddress(ms_addr, 'shared-addr-'+addr_style, True) + bitcoind.supply_wallet.sendtoaddress(address=ms_addr, amount=5) + bitcoind.supply_wallet.generatetoaddress(101, bitcoind.supply_wallet.getnewaddress()) # mining + unspent = bitcoind.supply_wallet.listunspent(addresses=[ms_addr]) + ret_addr = bitcoind.supply_wallet.getrawchangeaddress() ''' If you get insufficent funds, even tho we provide the UTXO (!!), do this: @@ -1574,11 +1541,13 @@ def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, got from non-multisig to multisig on same bitcoin-qt instance). -> Now doing that, automated, above. ''' - resp = bitcoind.walletcreatefundedpsbt([dict(txid=t, vout=o) for t,o in avail], - [{ret_addr: amt/1E8}], 0, + resp = bitcoind.supply_wallet.walletcreatefundedpsbt([dict(txid=unspent[0]["txid"], vout=unspent[0]["vout"])], + [{ret_addr: 2}], 0, {'subtractFeeFromOutputs': [0], 'includeWatching': True}, True) - assert resp['changepos'] == -1 + resp = bitcoind.supply_wallet.walletprocesspsbt(resp["psbt"]) + + # assert resp['changepos'] == -1 psbt = b64decode(resp['psbt']) open('debug/funded.psbt', 'wb').write(psbt) @@ -1598,19 +1567,21 @@ def test_bitcoind_cosigning(dev, bitcoind, import_ms_wallet, clear_ms, explora, open('debug/cc-updated.psbt', 'wb').write(updated) - # have bitcoind do the rest of the signing - rr = bitcoind.walletprocesspsbt(b64encode(updated).decode('ascii')) - pprint(rr) - - open('debug/bc-processed.psbt', 'wt').write(rr['psbt']) - assert rr['complete'] + # # have bitcoind do the rest of the signing + # rr = bitcoind.supply_wallet.walletprocesspsbt(b64encode(updated).decode('ascii')) + # pprint(rr) + # + # open('debug/bc-processed.psbt', 'wt').write(rr['psbt']) + # assert rr['complete'] + # TODO I have moved this up - so that bitcoind signs first, if it signed second it failed with + # TODO "Specified sighash value does not match value stored in PSBT" # finalize and send - rr = bitcoind.finalizepsbt(rr['psbt'], True) + rr = bitcoind.supply_wallet.finalizepsbt(b64encode(updated).decode('ascii'), True) open('debug/bc-final-txn.txn', 'wt').write(rr['hex']) assert rr['complete'] - txn_id = bitcoind.sendrawtransaction(rr['hex']) + txn_id = bitcoind.supply_wallet.sendrawtransaction(rr['hex']) print(txn_id) @pytest.mark.parametrize('addr_fmt', [AF_P2WSH] ) diff --git a/testing/test_paper.py b/testing/test_paper.py index 2eecf551..b77b0b3b 100644 --- a/testing/test_paper.py +++ b/testing/test_paper.py @@ -95,7 +95,7 @@ def test_generate(mode, pdf, dev, cap_menu, pick_menu_item, goto_home, cap_story addr = Key.from_text(val) else: hrp, data, enc = bech32_decode(val) - assert hrp in {'tb', 'bc' } + assert hrp in {'tb', 'bc', 'bcrt'} assert enc == Encoding.BECH32 decoded = convertbits(data[1:], 5, 8, False)[-20:] addr = Key(hash160=bytes(decoded), is_compressed=True, netcode='XTN') diff --git a/testing/test_sign.py b/testing/test_sign.py index 2a1e2f68..76f0593f 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -184,9 +184,10 @@ if 0: @pytest.mark.bitcoind +@pytest.mark.veryslow @pytest.mark.parametrize('segwit', [True, False]) @pytest.mark.parametrize('out_style', ADDR_STYLES) -def test_io_size(request, decode_with_bitcoind, fake_txn, is_mark3, is_mark4, +def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn, is_mark3, is_mark4, start_sign, end_sign, dev, segwit, out_style, accept = True): # try a bunch of different bigger sized txns @@ -198,6 +199,9 @@ def test_io_size(request, decode_with_bitcoind, fake_txn, is_mark3, is_mark4, # - only mk3 can do full amounts # - time on mk3, v4.0.0 firmware: 13 minutes + # for this test you need to configure core `repcservertimeout` to something big + # in bitcoin.conf `rpcservertimeout=2000` should do the trick + use_regtest() num_in = 10 num_out = 10 @@ -261,7 +265,7 @@ def test_io_size(request, decode_with_bitcoind, fake_txn, is_mark3, is_mark4, @pytest.mark.bitcoind @pytest.mark.parametrize('num_ins', [ 2, 7, 15 ]) @pytest.mark.parametrize('segwit', [True, False]) -def test_real_signing(fake_txn, try_sign, dev, num_ins, segwit, decode_with_bitcoind): +def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit, decode_with_bitcoind): # create a TXN using actual addresses that are correct for DUT xp = dev.master_xpub @@ -285,11 +289,11 @@ def test_real_signing(fake_txn, try_sign, dev, num_ins, segwit, decode_with_bitc @pytest.mark.parametrize('we_finalize', [ False, True ]) @pytest.mark.parametrize('num_dests', [ 1, 10, 25 ]) @pytest.mark.bitcoind -def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, end_sign, we_finalize, num_dests): +def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, start_sign, end_sign, we_finalize, num_dests): wallet_xfp = match_key() - - bal = bitcoind.getbalance() + use_regtest() + bal = bitcoind.supply_wallet.getbalance() assert bal > 0, "need some play money; drink from a faucet" amt = round((bal/4)/num_dests, 6) @@ -297,8 +301,8 @@ def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, en args = {} for no in range(num_dests): - dest = bitcoind.getrawchangeaddress() - assert dest[0] in '2mn' or dest.startswith('tb1'), dest + dest = bitcoind.supply_wallet.getrawchangeaddress() + assert dest.startswith('bcrt1'), dest args[dest] = amt @@ -306,11 +310,11 @@ def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, en # old approach: fundraw + convert to psbt # working with hex strings here - txn = bitcoind.createrawtransaction([], args) + txn = bitcoind.supply_wallet.createrawtransaction([], args) assert txn[0:2] == '02' #print(txn) - resp = bitcoind.fundrawtransaction(txn) + resp = bitcoind.supply_wallet.fundrawtransaction(txn) txn2 = resp['hex'] fee = resp['fee'] chg_pos = resp['changepos'] @@ -318,11 +322,11 @@ def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, en print("Sending %.8f XTN to %s (Change back in position: %d)" % (amt, dest, chg_pos)) - psbt = b64decode(bitcoind.converttopsbt(txn2, True)) + psbt = b64decode(bitcoind.supply_wallet.converttopsbt(txn2, True)) # use walletcreatefundedpsbt # - updated/validated against 0.17.1 - resp = bitcoind.walletcreatefundedpsbt([], args, 0, { + resp = bitcoind.supply_wallet.walletcreatefundedpsbt([], args, 0, { 'subtractFeeFromOutputs': list(range(num_dests)), 'feeRate': 0.00001500}, True) @@ -370,7 +374,7 @@ def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, en assert b4 != aft, "signing didn't change anything?" open('debug/signed.psbt', 'wb').write(signed) - resp = bitcoind.finalizepsbt(str(b64encode(signed), 'ascii'), True) + resp = bitcoind.supply_wallet.finalizepsbt(str(b64encode(signed), 'ascii'), True) #combined_psbt = b64decode(resp['psbt']) #open('debug/combined.psbt', 'wb').write(combined_psbt) @@ -384,7 +388,7 @@ def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, en open('debug/finalized-by-btcd.txn', 'wb').write(network) # try to send it - txed = bitcoind.sendrawtransaction(B2A(network)) + txed = bitcoind.supply_wallet.sendrawtransaction(B2A(network)) print("Final txn hash: %r" % txed) else: @@ -392,7 +396,7 @@ def test_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, en #print("Final txn: %s" % B2A(signed)) open('debug/finalized-by-cc.txn', 'wb').write(signed) - txed = bitcoind.sendrawtransaction(B2A(signed)) + txed = bitcoind.supply_wallet.sendrawtransaction(B2A(signed)) print("Final txn hash: %r" % txed) def test_sign_example(set_master_key, sim_execfile, start_sign, end_sign): @@ -422,7 +426,7 @@ def test_sign_example(set_master_key, sim_execfile, start_sign, end_sign): @pytest.mark.bitcoind @pytest.mark.unfinalized -def test_sign_p2sh_p2wpkh(match_key, start_sign, end_sign, bitcoind): +def test_sign_p2sh_p2wpkh(match_key, use_regtest, start_sign, end_sign, bitcoind): # Check we can finalize p2sh_p2wpkh inputs right. # TODO fix this @@ -447,7 +451,7 @@ def test_sign_p2sh_p2wpkh(match_key, start_sign, end_sign, bitcoind): # use bitcoind to combine open('debug/signed.psbt', 'wb').write(signed_psbt) - resp = bitcoind.finalizepsbt(str(b64encode(signed_psbt), 'ascii'), True) + resp = bitcoind.rpc.finalizepsbt(str(b64encode(signed_psbt), 'ascii'), True) assert resp['complete'] == True, "bitcoind wasn't able to finalize it" network = a2b_hex(resp['hex']) @@ -458,7 +462,7 @@ def test_sign_p2sh_p2wpkh(match_key, start_sign, end_sign, bitcoind): @pytest.mark.bitcoind @pytest.mark.unfinalized -def test_sign_p2sh_example(set_master_key, sim_execfile, start_sign, end_sign, decode_psbt_with_bitcoind, offer_ms_import, need_keypress, clear_ms): +def test_sign_p2sh_example(set_master_key, use_regtest, sim_execfile, start_sign, end_sign, decode_psbt_with_bitcoind, offer_ms_import, need_keypress, clear_ms): # Use the private key given in BIP 174 and do similar signing # as the examples. @@ -537,7 +541,7 @@ def test_sign_p2sh_example(set_master_key, sim_execfile, start_sign, end_sign, d @pytest.mark.bitcoind -def test_change_case(start_sign, end_sign, check_against_bitcoind, cap_story): +def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind, cap_story): # is change shown/hidden at right times. no fraud checks # NOTE: out#1 is change: @@ -581,7 +585,7 @@ def test_change_case(start_sign, end_sign, check_against_bitcoind, cap_story): @pytest.mark.parametrize('case', [ 1, 2]) @pytest.mark.bitcoind -def test_change_fraud_path(start_sign, end_sign, case, check_against_bitcoind, cap_story): +def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_against_bitcoind, cap_story): # fraud: BIP-32 path of output doesn't lead to pubkey indicated # NOTE: out#1 is change: @@ -625,7 +629,7 @@ def test_change_fraud_path(start_sign, end_sign, case, check_against_bitcoind, c signed = end_sign(True) @pytest.mark.bitcoind -def test_change_fraud_addr(start_sign, end_sign, check_against_bitcoind, cap_story): +def test_change_fraud_addr(start_sign, end_sign, use_regtest, check_against_bitcoind, cap_story): # fraud: BIP-32 path of output doesn't match TXO address from pycoin.tx.Tx import Tx from pycoin.tx.TxOut import TxOut @@ -659,11 +663,10 @@ def test_change_fraud_addr(start_sign, end_sign, check_against_bitcoind, cap_sto @pytest.mark.parametrize('case', [ 'p2wpkh', 'p2sh']) @pytest.mark.bitcoind -def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, cap_story, case): +def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, use_regtest, cap_story, case): # not fraud: output address encoded in various equiv forms from pycoin.tx.Tx import Tx - from pycoin.tx.TxOut import TxOut - + use_regtest() # NOTE: out#1 is change: #chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' @@ -678,7 +681,7 @@ def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, cap_st t.txs_out[1].script = bytes([0, 20]) + bytes(pkh) from bech32 import encode - expect_addr = encode('tb', 0, pkh) + expect_addr = encode('bcrt', 0, pkh) elif case == 'p2sh': @@ -916,12 +919,13 @@ def KEEP_test_random_psbt(try_sign, sim_exec, fname="data/ .psbt"): @pytest.mark.bitcoind @pytest.mark.unfinalized @pytest.mark.parametrize('num_dests', [ 1, 10, 25 ]) -def test_finalization_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, start_sign, end_sign, num_dests): +def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, start_sign, end_sign, num_dests): # Compare how we finalize vs bitcoind ... should be exactly the same txn - wallet_xfp = match_key() + # has to be after match key + use_regtest() - bal = bitcoind.getbalance() + bal = bitcoind.supply_wallet.getbalance() assert bal > 0, "need some play money; drink from a faucet" amt = round((bal/4)/num_dests, 6) @@ -929,14 +933,14 @@ def test_finalization_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, s args = {} for no in range(num_dests): - dest = bitcoind.getrawchangeaddress() - assert dest[0] in '2mn' or dest.startswith('tb1'), dest + dest = bitcoind.supply_wallet.getrawchangeaddress() + assert dest.startswith('bcrt1q'), dest args[dest] = amt # use walletcreatefundedpsbt # - updated/validated against 0.17.1 - resp = bitcoind.walletcreatefundedpsbt([], args, 0, { + resp = bitcoind.supply_wallet.walletcreatefundedpsbt([], args, 0, { 'subtractFeeFromOutputs': list(range(num_dests)), 'feeRate': 0.00001500}, True) @@ -958,9 +962,7 @@ def test_finalization_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, s # pull out included txn txn2 = B2A(mine.txn) - start_sign(psbt, finalize=True) - # verify against how bitcoind reads it check_against_bitcoind(txn2, fee) @@ -975,7 +977,7 @@ def test_finalization_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, s open('debug/vs-signed-unfin.psbt', 'wb').write(signed) # Use bitcoind to finalize it this time. - resp = bitcoind.finalizepsbt(str(b64encode(signed), 'ascii'), True) + resp = bitcoind.supply_wallet.finalizepsbt(str(b64encode(signed), 'ascii'), True) assert resp['complete'] == True, "bitcoind wasn't able to finalize it" network = a2b_hex(resp['hex']) @@ -987,7 +989,7 @@ def test_finalization_vs_bitcoind(match_key, check_against_bitcoind, bitcoind, s assert network == signed_final, "Finalized differently" # try to send it - txed = bitcoind.sendrawtransaction(B2A(network)) + txed = bitcoind.supply_wallet.sendrawtransaction(B2A(network)) print("Final txn hash: %r" % txed)