diff --git a/.cirrus.yml b/.cirrus.yml index 63af00c21..57ddb3751 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -52,16 +52,19 @@ test_task: - apt-get update && apt-get install -y --no-install-recommends python3-dev python3-pip wget << : *PREP_STUFF_TEMPLATE test_script: - - pwd - - ls -l ./.env/bin - source ./.env/bin/activate - echo $PATH #- pip3 install -e . - - pytest --cov=cryptoadvance + - pytest --cov=cryptoadvance --junitxml=./testresults.xml + always: + junit_artifacts: + path: "./testresults.xml" + format: junit + cypress_test_task: container: - image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python:focal + image: registry.gitlab.com/cryptoadvance/specter-desktop/cypress-python:latest pre_prep_script: - apt-get update && apt-get install -y --no-install-recommends python3-dev python3-pip python3-virtualenv bc # The stupid old debian-package is not installing a proper binary but just the python-package diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c43c546ff..4fb1c8f00 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -172,6 +172,13 @@ pytest tests/test_specter.py::test_specter pytest --setup-show ``` +Get the log-output of bitcoind side by side with the test-output. For sure you will only see the logs if the test fails. +``` +pytest --bitcoind-log-stdout +# Probably better to redirect into a file +pytest --bitcoind-log-stdout > testoutput.log +``` + Check the cypress-section on how to run cypress-frontend-tests. ## Code-Style diff --git a/cypress.json b/cypress.json index 0bff24c4b..3d468c56e 100644 --- a/cypress.json +++ b/cypress.json @@ -10,5 +10,8 @@ "spec_elm_single_segwit_wallet.js", "spec_elm_multi_segwit_wallet.js" ], - "baseUrl": "http://localhost:25444" + "baseUrl": "http://localhost:25444", + "env": { + "broadcast_timeout":"8000" + } } diff --git a/cypress/integration/spec_elm_multi_segwit_wallet.js b/cypress/integration/spec_elm_multi_segwit_wallet.js index d89ea86d5..bf76fb9b6 100644 --- a/cypress/integration/spec_elm_multi_segwit_wallet.js +++ b/cypress/integration/spec_elm_multi_segwit_wallet.js @@ -1,7 +1,5 @@ describe('Operating with an elements multisig wallet', () => { - // 4000ms was often not enough for waiting a elm-transaction - // So let's try double as much: - const broadcast_timeout = 8000 + it('Creates an two elements multisig hot wallets (segwit/nested) both 2/3', () => { cy.viewport(1200,660) @@ -84,7 +82,7 @@ describe('Operating with an elements multisig wallet', () => { cy.get('#broadcast_local_btn').click() // gets redirected to "transactions" - cy.get('#fullbalance_amount', { timeout: broadcast_timeout }) + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) @@ -127,7 +125,7 @@ describe('Operating with an elements multisig wallet', () => { cy.get('#broadcast_local_btn').click() // gets redirected to "transactions" - cy.get('#fullbalance_amount', { timeout: broadcast_timeout }) + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) @@ -169,7 +167,7 @@ describe('Operating with an elements multisig wallet', () => { cy.get('#broadcast_local_btn').click() // gets redirected to "transactions" - cy.get('#fullbalance_amount', { timeout: broadcast_timeout }) + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) @@ -211,7 +209,7 @@ describe('Operating with an elements multisig wallet', () => { cy.get('#send_tx_btn').click() cy.get('#broadcast_local_btn').click() // gets redirected to "transactions" - cy.get('#fullbalance_amount', { timeout: broadcast_timeout}) + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) diff --git a/cypress/integration/spec_elm_single_segwit_wallet.js b/cypress/integration/spec_elm_single_segwit_wallet.js index 8c8dff031..c37a25ec1 100644 --- a/cypress/integration/spec_elm_single_segwit_wallet.js +++ b/cypress/integration/spec_elm_single_segwit_wallet.js @@ -31,7 +31,7 @@ describe('Operating with an elements singlesig wallet', () => { cy.get('#hot_elements_device_1_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() - cy.get('#fullbalance_amount') + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) @@ -56,7 +56,7 @@ describe('Operating with an elements singlesig wallet', () => { cy.get('#hot_elements_device_1_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() - cy.get('#fullbalance_amount') + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) @@ -77,7 +77,7 @@ describe('Operating with an elements singlesig wallet', () => { cy.get('#hot_elements_device_1_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() - cy.get('#fullbalance_amount') + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) @@ -102,7 +102,7 @@ describe('Operating with an elements singlesig wallet', () => { cy.get('#hot_elements_device_1_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() - cy.get('#fullbalance_amount') + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const newBalance = parseFloat($div.text()) expect(newBalance).to.be.lte(oldBalance - 1.5) diff --git a/cypress/integration/spec_wallet_send.js b/cypress/integration/spec_wallet_send.js index 861c34299..1a5957ca6 100644 --- a/cypress/integration/spec_wallet_send.js +++ b/cypress/integration/spec_wallet_send.js @@ -35,7 +35,7 @@ describe('Send transactions from bitcoin hotwallets', () => { cy.get('#hot_device_1_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() - cy.get('#fullbalance_amount') + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const n = parseFloat($div.text()) expect(n).to.be.equals(0) @@ -78,7 +78,7 @@ describe('Send transactions from bitcoin hotwallets', () => { cy.get('#hot_device_1_hot_sign_btn').click() cy.get('#hot_enter_passphrase__submit').click() cy.get('#broadcast_local_btn').click() - cy.get('#fullbalance_amount') + cy.get('#fullbalance_amount', { timeout: Cypress.env("broadcast_timeout") }) .should(($div) => { const n = parseFloat($div.text()) expect(n).to.be.equals(0) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7a596023e..9d7e5f8f3 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -69,7 +69,7 @@ Cypress.Commands.add("addHotDevice", (name, node_type) => { cy.get('#submit-mnemonic').click() cy.get('#device_name').type(name) cy.get('#submit-keys').click() - cy.get('#devices_list > .item > div').contains(name) + cy.get('#devices_list > .item > div', { timeout: 8000 }).contains(name) }) }) @@ -136,7 +136,7 @@ Cypress.Commands.add("mine2wallet", (chain) => { } cy.wait(15000) cy.reload() - cy.get('#fullbalance_amount') + cy.get('#fullbalance_amount') // Wait 5 secs + 15 secs timeout .should(($div) => { const n = parseFloat($div.text()) expect(n).to.be.gt(oldBalance) diff --git a/docker/cypress-python/Dockerfile b/docker/cypress-python/Dockerfile index b740db6c1..b1e77cbee 100644 --- a/docker/cypress-python/Dockerfile +++ b/docker/cypress-python/Dockerfile @@ -2,7 +2,7 @@ FROM registry.gitlab.com/cryptoadvance/specter-desktop/cypress-base-ubuntu-focal RUN apt-get update && apt-get install -y \ python3-pip python3-virtualenv zip unzip file apt libusb-1.0-0-dev libudev-dev \ - bc libevent-2.1-7 + bc libevent-2.1-7 jq wget curl # Stuff needed for Elements (compilation) RUN DEBIAN_FRONTEND="noninteractive" apt-get install --no-install-recommends -y bsdmainutils libboost-test-dev libboost-filesystem-dev libboost-thread-dev libsqlite3-dev git libevent-pthreads-2.1-7 diff --git a/pytest.ini b/pytest.ini index 982f2d6fd..6da7c3c66 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -norecursedirs = tests/bitcoin tests/elements +norecursedirs = tests/bitcoin* tests/elements log_format = [%(levelname)8s] %(message)s %(name)s (%(filename)s:%(lineno)s) addopts = --bitcoind-version v0.20.1 --elementsd-version v0.20.99 markers = diff --git a/src/cryptoadvance/specter/config.py b/src/cryptoadvance/specter/config.py index de4a078d4..a710af277 100644 --- a/src/cryptoadvance/specter/config.py +++ b/src/cryptoadvance/specter/config.py @@ -62,6 +62,10 @@ class BaseConfig(object): "ELMD_REGTEST_DATA_DIR", "/tmp/specter_elm_regtest_plain_datadir" ) + # the default timeout for Bitcoin/Liquid RPC-calls + BITCOIN_RPC_TIMEOUT = int(os.getenv("BITCOIN_RPC_TIMEOUT", "10")) + LIQUID_RPC_TIMEOUT = int(os.getenv("LIQUID_RPC_TIMEOUT", "10")) + # The self-signed ssl-certificate which is lazily created is configurable to a certain extent SPECTER_SSL_CERT_SUBJECT_C = os.getenv("SPECTER_SSL_CERT_SUBJECT_C", "DE") SPECTER_SSL_CERT_SUBJECT_ST = os.getenv("SPECTER_SSL_CERT_SUBJECT_ST", "BDW") @@ -148,6 +152,10 @@ class TestConfig(BaseConfig): # API active by default in test-mode SPECTER_API_ACTIVE = _get_bool_env_var("SPECTER_API_ACTIVE", "True") + # See #1316 since Bitcoin v0.21.1 (not only) the importmulti-call takes longer than 10 seconds on cirrus + BITCOIN_RPC_TIMEOUT = 20 + LIQUID_RPC_TIMEOUT = 30 + class CypressTestConfig(TestConfig): SPECTER_DATA_FOLDER = os.path.expanduser( @@ -165,6 +173,8 @@ class CypressTestConfig(TestConfig): BTCD_REGTEST_DATA_DIR = os.getenv( "BTCD_REGTEST_DATA_DIR", "/tmp/specter_cypress_elm_regtest_plain_datadir" ) + BITCOIN_RPC_TIMEOUT = 30 + LIQUID_RPC_TIMEOUT = 40 class ProductionConfig(BaseConfig): diff --git a/src/cryptoadvance/specter/liquid/rpc.py b/src/cryptoadvance/specter/liquid/rpc.py index c5a5aa002..5a18cb86a 100644 --- a/src/cryptoadvance/specter/liquid/rpc.py +++ b/src/cryptoadvance/specter/liquid/rpc.py @@ -561,7 +561,7 @@ class LiquidRPC(BitcoinRPC): port=rpc.port, protocol=rpc.protocol, path=rpc.path, - timeout=rpc.timeout, + timeout=cls.default_timeout, # Elements is slower session=rpc.session, proxy_url=rpc.proxy_url, only_tor=rpc.only_tor, diff --git a/src/cryptoadvance/specter/node.py b/src/cryptoadvance/specter/node.py index 6395db8be..609a6b38b 100644 --- a/src/cryptoadvance/specter/node.py +++ b/src/cryptoadvance/specter/node.py @@ -172,8 +172,14 @@ class Node: if is_liquid(res.get("chain")): # convert to LiquidRPC class rpc = LiquidRPC.from_bitcoin_rpc(rpc) + except RpcError as rpce: + if rpce.status_code == 401: + return rpc # The user is failing to configure correctly + logger.exception(rpce) + return None except Exception as e: - return rpc + logger.exception(e) + return None if rpc.test_connection(): return rpc else: @@ -335,20 +341,21 @@ class Node: rpc.listwallets() r["tests"]["wallets"] = True except RpcError as rpce: - logger.error(rpce) + logger.info(f"Couldn't list wallets while test_rpc {rpce}") r["tests"]["wallets"] = False r["err"] = "Wallets disabled" r["out"] = json.dumps(rpc.getblockchaininfo(), indent=4) except ConnectionError as e: - logger.error("Caught an ConnectionError while test_rpc: %s", e) + logger.info("Caught an ConnectionError while test_rpc: %s", e) r["tests"]["connectable"] = False r["err"] = _("Failed to connect!") r["code"] = -1 except RpcError as rpce: - logger.error("Caught an RpcError while test_rpc: %s", rpce) - logger.error(rpce.status_code) + logger.info( + f"Caught an RpcError while test_rpc status_code: {rpce.status_code} error_code:{rpce.error_code}" + ) r["tests"]["connectable"] = True r["code"] = rpc.r.status_code if rpce.status_code == 401: diff --git a/src/cryptoadvance/specter/rpc.py b/src/cryptoadvance/specter/rpc.py index 40c35f76d..49f071aef 100644 --- a/src/cryptoadvance/specter/rpc.py +++ b/src/cryptoadvance/specter/rpc.py @@ -1,7 +1,8 @@ import logging -import requests, json, os +import requests, urllib3, json, os import os, sys, errno from .helpers import is_ip_private +from .specter_error import SpecterError logger = logging.getLogger(__name__) @@ -189,6 +190,7 @@ class RpcError(Exception): try: rpc.does_not_exist() except RpcError as rpce: + assert rpce.status_code == 401 # A https-status-code assert rpce.error_code == -32601 assert rpce.error_msg == "Method not found" See for error_codes https://github.com/bitcoin/bitcoin/blob/v0.15.0.1/src/rpc/protocol.h#L32L87 @@ -214,9 +216,14 @@ class RpcError(Exception): class BitcoinRPC: counter = 0 + # These are used for tracing the calls without too many duplicates last_call_hash = None last_call_hash_counter = 0 + # https://docs.python-requests.org/en/master/user/quickstart/#timeouts + # None means until connection closes. It's specified in seconds + default_timeout = None # seconds + def __init__( self, user="bitcoin", @@ -238,7 +245,7 @@ class BitcoinRPC: self.protocol = protocol self.host = host self.path = path - self.timeout = timeout + self.timeout = timeout or self.__class__.default_timeout self.proxy_url = proxy_url self.only_tor = only_tor self.r = None @@ -338,9 +345,25 @@ class BitcoinRPC: if "wallet" in kwargs: url = url + "/wallet/{}".format(kwargs["wallet"]) self.trace_call(url, payload) - r = self.session.post( - url, data=json.dumps(payload), headers=headers, timeout=timeout - ) + try: + r = self.session.post( + url, data=json.dumps(payload), headers=headers, timeout=timeout + ) + except (requests.exceptions.Timeout, urllib3.exceptions.ReadTimeoutError) as to: + # Timeout is effectively one of the two: + # ConnectTimeout: The request timed out while trying to connect to the remote server + # ReadTimeout: The server did not send any data in the allotted amount of time. + # ReadTimeoutError: Raised when a socket timeout occurs while receiving data from a server + logger.error( + "Timeout while {} call({: <28}) payload:{} Exception: {}".format( + self.__class__.__name__, "/".join(url.split("/")[3:]), payload, to + ) + ) + raise SpecterError( + "Timeout while {} call({: <28}) payload:{}".format( + self.__class__.__name__, "/".join(url.split("/")[3:]), payload + ) + ) self.r = r if r.status_code != 200: logger.debug(f"last call FAILED: {r.text} (raising RpcError)") diff --git a/src/cryptoadvance/specter/server.py b/src/cryptoadvance/specter/server.py index bc08b552d..085e648b2 100644 --- a/src/cryptoadvance/specter/server.py +++ b/src/cryptoadvance/specter/server.py @@ -9,6 +9,9 @@ from flask import Flask, redirect, request, url_for, jsonify, session from flask_babel import Babel from flask_login import LoginManager, login_user from flask_wtf.csrf import CSRFProtect +from cryptoadvance.specter.liquid.rpc import LiquidRPC + +from cryptoadvance.specter.rpc import BitcoinRPC from .helpers import hwi_get_config from .specter import Specter @@ -104,6 +107,9 @@ def init_app(app, hwibridge=False, specter=None): # Login via Flask-Login app.logger.info("Initializing LoginManager") app.secret_key = app.config["SECRET_KEY"] + BitcoinRPC.default_timeout = app.config["BITCOIN_RPC_TIMEOUT"] + LiquidRPC.default_timeout = app.config["LIQUID_RPC_TIMEOUT"] + if specter is None: # the default. If not None, then it got injected for testing app.logger.info("Initializing Specter") diff --git a/src/cryptoadvance/specter/util/common.py b/src/cryptoadvance/specter/util/common.py new file mode 100644 index 000000000..e5f6831fa --- /dev/null +++ b/src/cryptoadvance/specter/util/common.py @@ -0,0 +1,7 @@ +def str2bool(my_str): + """returns a reasonable boolean from a string so that "False" will result in False""" + if my_str is None: + return False + elif isinstance(my_str, str) and my_str.lower() == "false": + return False + return bool(my_str) diff --git a/tests/bitcoin_core/test/functional/test_framework/util.py b/tests/bitcoin_core/test/functional/test_framework/util.py index 9e6be0d0d..548e860e7 100644 --- a/tests/bitcoin_core/test/functional/test_framework/util.py +++ b/tests/bitcoin_core/test/functional/test_framework/util.py @@ -71,7 +71,11 @@ def create_lots_of_big_transactions(wallet, txouts, utxos, num, fee): for txout in txouts: tx.vout.append(txout) newtx = tx.serialize().hex() - signresult = wallet.devices[0].sign_raw_tx(newtx, wallet) - txid = node.sendrawtransaction(signresult["hex"], 0) + psbtF = wallet.rpc.converttopsbt(newtx) + psbtFF = wallet.rpc.walletprocesspsbt(psbtF) + signed = wallet.devices[0].sign_psbt(psbtFF["psbt"], wallet) + assert signed["complete"] + finalized = wallet.rpc.finalizepsbt(signed["psbt"]) + txid = node.sendrawtransaction(finalized["hex"]) txids.append(txid) return txids diff --git a/tests/conftest.py b/tests/conftest.py index 16a5492b5..00843021e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,13 +24,37 @@ from cryptoadvance.specter.server import create_app, init_app from cryptoadvance.specter.specter import Specter from cryptoadvance.specter.user import User from cryptoadvance.specter.util.wallet_importer import WalletImporter +from cryptoadvance.specter.util.common import str2bool +import code, traceback, signal pytest_plugins = ["ghost_machine"] +# This is from https://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application +# it enables stopping a hanging test via sending the pytest-process a SIGUSR2 (12) +# kill 12 pid-of-pytest +# In the article they claim to open a debug-console which didn't work for me but at least +# you get a stacktrace in the output. +def debug(sig, frame): + """Interrupt running process, and provide a python prompt for + interactive debugging.""" + d = {"_frame": frame} # Allow access to frame object. + d.update(frame.f_globals) # Unless shadowed by global + d.update(frame.f_locals) + + i = code.InteractiveConsole(d) + message = "Signal received : entering python shell.\nTraceback:\n" + message += "".join(traceback.format_stack(frame)) + i.interact(message) + + +def listen(): + signal.signal(signal.SIGUSR2, debug) # Register handler + def pytest_addoption(parser): """Internally called to add options to pytest see pytest_generate_tests(metafunc) on how to check that + Also used to register the SIGUSR2 (12) as decribed in conftest.py """ parser.addoption("--docker", action="store_true", help="run bitcoind in docker") parser.addoption( @@ -39,12 +63,19 @@ def pytest_addoption(parser): default="v0.20.1", help="Version of bitcoind (something which works with git checkout ...)", ) + parser.addoption( + "--bitcoind-log-stdout", + action="store", + default=False, + help="Whether bitcoind should log to stdout (default:False)", + ) parser.addoption( "--elementsd-version", action="store", default="master", help="Version of elementsd (something which works with git checkout ...)", ) + listen() def pytest_generate_tests(metafunc): @@ -61,6 +92,7 @@ def pytest_generate_tests(metafunc): def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[]): # logging.getLogger().setLevel(logging.DEBUG) requested_version = request.config.getoption("--bitcoind-version") + log_stdout = str2bool(request.config.getoption("--bitcoind-log-stdout")) if docker: from cryptoadvance.specter.process_controller.bitcoind_docker_controller import ( BitcoindDockerController, @@ -83,7 +115,10 @@ def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[ rpcport=rpcport ) # Alternatively take the one on the path for now bitcoind_controller.start_bitcoind( - cleanup_at_exit=True, cleanup_hard=True, extra_args=extra_args + cleanup_at_exit=True, + cleanup_hard=True, + extra_args=extra_args, + log_stdout=log_stdout, ) assert not bitcoind_controller.datadir is None running_version = bitcoind_controller.version() @@ -367,6 +402,7 @@ def specter_regtest_configured(bitcoin_regtest, devices_filled_data_folder): bitcoin_regtest.mine() # Realize that the wallet has funds: wallet.update() + assert wallet.fullbalance >= 20 assert not specter.wallet_manager.working_folder is None try: yield specter diff --git a/tests/test_persistence.py b/tests/test_persistence.py index fa808c35b..4177ee10e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -73,9 +73,11 @@ def test_write_device(app): "original": "tpubDDZ5jjGT5RvrAyjoLZfdCfv1PAPmicnhNctwZGKiCMF1Zy5hCGMqppxwYZzWgvPqk7LucMMHo7rkB6Dyj5ZLd2W62FAEP3U6pV4jD5gb9ma" } ) - app.specter.device_manager.add_device("some_name", "the_type", [a_key, another_key]) + app.specter.device_manager.add_device( + "some_name2", "the_type", [a_key, another_key] + ) write_device( - app.specter.device_manager.get_by_alias("some_name"), + app.specter.device_manager.get_by_alias("some_name2"), "/tmp/delete_me_test_file.json", ) os.remove("/tmp/delete_me_test_file.json") diff --git a/tests/test_rpc.py b/tests/test_rpc.py index bbe533d1c..536a22c77 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -1,6 +1,7 @@ import pytest from cryptoadvance.specter.rpc import BitcoinRPC, RpcError +from cryptoadvance.specter.specter_error import SpecterError def test_BitcoinRpc(bitcoin_regtest): @@ -23,3 +24,22 @@ def test_BitcoinRpc(bitcoin_regtest): except RpcError as rpce: assert rpce.error_code == -32601 assert rpce.error_msg == "Method not found" + + +def test_BitcoinRpc_timeout(bitcoin_regtest, caplog): + brt = bitcoin_regtest # stupid long name + rpc = BitcoinRPC( + brt.rpcconn.rpcuser, + brt.rpcconn.rpcpassword, + host=brt.rpcconn.ipaddress, + port=brt.rpcconn.rpcport, + ) + rpc.timeout = 0.0000000000001 + try: + rpc.createwallet("some_test_wallet_name_392") + assert False, "Should raise an exception" + except SpecterError: + assert ( + "Timeout while BitcoinRPC call( ) payload:[{'method': 'createwallet', 'params': ['some_test_wallet_name_392'], 'jsonrpc': '2.0', 'id': 0}]" + in caplog.text + ) diff --git a/tests/test_specter.py b/tests/test_specter.py index 1716bc735..5bc999d5e 100644 --- a/tests/test_specter.py +++ b/tests/test_specter.py @@ -1,4 +1,4 @@ -import json, logging, pytest, time, os +import json, logging, pytest from decimal import Decimal from cryptoadvance.specter.helpers import alias, generate_mnemonic from cryptoadvance.specter.key import Key @@ -6,7 +6,6 @@ from cryptoadvance.specter.rpc import BitcoinRPC from cryptoadvance.specter.specter import Specter from cryptoadvance.specter.specter_error import SpecterError from cryptoadvance.specter.managers.wallet_manager import WalletManager -from conftest import instantiate_bitcoind_controller def test_alias(): @@ -36,6 +35,7 @@ def test_abandon_purged_tx( # from the mempool. Test starts a new bitcoind with a restricted mempool to make it # easier to spam the mempool and purge our target tx. # TODO: Similar test but for maxmempoolexpiry? + # Copied and adapted from: # https://github.com/bitcoin/bitcoin/blob/master/test/functional/mempool_limit.py from bitcoin_core.test.functional.test_framework.util import ( @@ -43,6 +43,7 @@ def test_abandon_purged_tx( satoshi_round, create_lots_of_big_transactions, ) + from conftest import instantiate_bitcoind_controller caplog.set_level(logging.DEBUG) @@ -52,156 +53,150 @@ def test_abandon_purged_tx( bitcoind_controller = instantiate_bitcoind_controller( docker, request, - rpcport=18968, + rpcport=18998, extra_args=["-acceptnonstdtxn=1", "-maxmempool=5", "-spendzeroconfchange=0"], ) + rpcconn = bitcoind_controller.rpcconn + rpc = rpcconn.get_rpc() + assert rpc is not None + assert rpc.ipaddress != None + + # Note: Our utxo creation is simpler than mempool_limit.py's approach since we're + # running in regtest and can just use generatetoaddress(). + + # Instantiate a new Specter instance to talk to this bitcoind + config = { + "rpc": { + "autodetect": False, + "datadir": "", + "user": rpcconn.rpcuser, + "password": rpcconn.rpcpassword, + "port": rpcconn.rpcport, + "host": rpcconn.ipaddress, + "protocol": "http", + }, + "auth": { + "method": "rpcpasswordaspin", + }, + } + specter = Specter(data_folder=devices_filled_data_folder, config=config) + specter.check() + + assert specter.info["mempool_info"]["maxmempool"] == 5 * 1000 * 1000 # 5MB + + # Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt. + # TODO: Make a test fixture in conftest.py that sets up already funded wallets + # for a bitcoin core hot wallet. + wallet_manager = WalletManager( + 200100, + devices_filled_data_folder, + rpc, + "regtest", + device_manager, + allow_threading=False, + ) + + # Create a new device that can sign psbts (Bitcoin Core hot wallet) + device = device_manager.add_device( + name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[] + ) + device.setup_device(file_password=None, wallet_manager=wallet_manager) + device.add_hot_wallet_keys( + mnemonic=generate_mnemonic(strength=128), + passphrase="", + paths=["m/49h/0h/0h"], + file_password=None, + wallet_manager=wallet_manager, + testnet=True, + keys_range=[0, 1000], + keys_purposes=[], + ) + + wallet = wallet_manager.create_wallet( + "bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device] + ) + + # Fund the wallet. Going to need a LOT of utxos to play with. + logging.info("Generating utxos to wallet") + address = wallet.getnewaddress() + wallet.rpc.generatetoaddress(91, address) + + # newly minted coins need 100 blocks to get spendable + # let's mine another 100 blocks to get these coins spendable + wallet.rpc.generatetoaddress(101, address) + + # update the wallet data + wallet.get_balance() + + # ==== Begin test from mempool_limit.py ==== + txouts = gen_return_txouts() + relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) + + logging.info("Check that mempoolminfee is minrelytxfee") + assert satoshi_round(rpc.getmempoolinfo()["minrelaytxfee"]) == Decimal("0.00001000") + assert satoshi_round(rpc.getmempoolinfo()["mempoolminfee"]) == Decimal("0.00001000") + + txids = [] + utxos = wallet.rpc.listunspent() + + logging.info("Create a mempool tx that will be evicted") + us0 = utxos.pop() + inputs = [{"txid": us0["txid"], "vout": us0["vout"]}] + outputs = {wallet.getnewaddress(): 0.0001} + tx = wallet.rpc.createrawtransaction(inputs, outputs) + wallet.rpc.settxfee(str(relayfee)) # specifically fund this tx with low fee + txF = wallet.rpc.fundrawtransaction(tx) + wallet.rpc.settxfee(0) # return to automatic fee selection + txFS = device.sign_raw_tx(txF["hex"], wallet) + txid = wallet.rpc.sendrawtransaction(txFS["hex"]) + + # ==== Specter-specific: can't abandon a valid pending tx ==== try: - assert bitcoind_controller.get_rpc().test_connection() - rpcconn = bitcoind_controller.rpcconn - rpc = rpcconn.get_rpc() - assert rpc is not None - assert rpc.ipaddress != None - - # Note: Our utxo creation is simpler than mempool_limit.py's approach since we're - # running in regtest and can just use generatetoaddress(). - - # Instantiate a new Specter instance to talk to this bitcoind - config = { - "rpc": { - "autodetect": False, - "datadir": "", - "user": rpcconn.rpcuser, - "password": rpcconn.rpcpassword, - "port": rpcconn.rpcport, - "host": rpcconn.ipaddress, - "protocol": "http", - }, - "auth": { - "method": "rpcpasswordaspin", - }, - } - specter = Specter(data_folder=devices_filled_data_folder, config=config) - specter.check() - - assert specter.info["mempool_info"]["maxmempool"] == 5 * 1000 * 1000 # 5MB - - # Largely copy-and-paste from test_wallet_manager.test_wallet_createpsbt. - # TODO: Make a test fixture in conftest.py that sets up already funded wallets - # for a bitcoin core hot wallet. - wallet_manager = WalletManager( - 200100, - devices_filled_data_folder, - rpc, - "regtest", - device_manager, - allow_threading=False, - ) - - # Create a new device that can sign psbts (Bitcoin Core hot wallet) - device = device_manager.add_device( - name="bitcoin_core_hot_wallet", device_type="bitcoincore", keys=[] - ) - device.setup_device(file_password=None, wallet_manager=wallet_manager) - device.add_hot_wallet_keys( - mnemonic=generate_mnemonic(strength=128), - passphrase="", - paths=["m/49h/0h/0h"], - file_password=None, - wallet_manager=wallet_manager, - testnet=True, - keys_range=[0, 1000], - keys_purposes=[], - ) - - wallet = wallet_manager.create_wallet( - "bitcoincore_test_wallet", 1, "sh-wpkh", [device.keys[0]], [device] - ) - - # Fund the wallet. Going to need a LOT of utxos to play with. - logging.info("Generating utxos to wallet") - address = wallet.getnewaddress() - wallet.rpc.generatetoaddress(91, address) - - # newly minted coins need 100 blocks to get spendable - # let's mine another 100 blocks to get these coins spendable - wallet.rpc.generatetoaddress(101, address) - - # update the wallet data - wallet.get_balance() - - # ==== Begin test from mempool_limit.py ==== - txouts = gen_return_txouts() - relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) - - logging.info("Check that mempoolminfee is minrelytxfee") - assert satoshi_round(rpc.getmempoolinfo()["minrelaytxfee"]) == Decimal( - "0.00001000" - ) - assert satoshi_round(rpc.getmempoolinfo()["mempoolminfee"]) == Decimal( - "0.00001000" - ) - - txids = [] - utxos = wallet.rpc.listunspent() - - logging.info("Create a mempool tx that will be evicted") - us0 = utxos.pop() - inputs = [{"txid": us0["txid"], "vout": us0["vout"]}] - outputs = {wallet.getnewaddress(): 0.0001} - tx = wallet.rpc.createrawtransaction(inputs, outputs) - wallet.rpc.settxfee(str(relayfee)) # specifically fund this tx with low fee - txF = wallet.rpc.fundrawtransaction(tx) - wallet.rpc.settxfee(0) # return to automatic fee selection - txFS = device.sign_raw_tx(txF["hex"], wallet) - txid = wallet.rpc.sendrawtransaction(txFS["hex"]) - - # ==== Specter-specific: can't abandon a valid pending tx ==== - try: - wallet.abandontransaction(txid) - except SpecterError as e: - assert "Cannot abandon" in str(e) - - # ==== Resume test from mempool_limit.py ==== - # Spam the mempool with big transactions! - relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) - base_fee = float(relayfee) * 100 - for i in range(3): - txids.append([]) - txids[i] = create_lots_of_big_transactions( - wallet, txouts, utxos[30 * i : 30 * i + 30], 30, (i + 1) * base_fee - ) - - logging.info("The tx should be evicted by now") - assert txid not in wallet.rpc.getrawmempool() - txdata = wallet.rpc.gettransaction(txid) - assert txdata["confirmations"] == 0 # confirmation should still be 0 - - # ==== Specter-specific: Verify purge and abandon ==== - assert wallet.is_tx_purged(txid) wallet.abandontransaction(txid) + except SpecterError as e: + assert "Cannot abandon" in str(e) - # tx will still be in the wallet but marked "abandoned" - txdata = wallet.rpc.gettransaction(txid) - for detail in txdata["details"]: - if detail["category"] == "send": - assert detail["abandoned"] + # ==== Resume test from mempool_limit.py ==== + # Spam the mempool with big transactions! + relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) + base_fee = float(relayfee) * 100 + for i in range(3): + txids.append([]) + txids[i] = create_lots_of_big_transactions( + wallet, txouts, utxos[30 * i : 30 * i + 30], 30, (i + 1) * base_fee + ) - # Can we now spend those same inputs? - outputs = {wallet.getnewaddress(): 0.0001} - tx = wallet.rpc.createrawtransaction(inputs, outputs) + logging.info("The tx should be evicted by now") + assert txid not in wallet.rpc.getrawmempool() + txdata = wallet.rpc.gettransaction(txid) + assert txdata["confirmations"] == 0 # confirmation should still be 0 - # Fund this tx with a high enough fee - relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) - wallet.rpc.settxfee(str(relayfee * Decimal("3.0"))) + # ==== Specter-specific: Verify purge and abandon ==== + assert wallet.is_tx_purged(txid) + wallet.abandontransaction(txid) - txF = wallet.rpc.fundrawtransaction(tx) - wallet.rpc.settxfee(0) # return to automatic fee selection - txFS = device.sign_raw_tx(txF["hex"], wallet) - txid = wallet.rpc.sendrawtransaction(txFS["hex"]) + # tx will still be in the wallet but marked "abandoned" + txdata = wallet.rpc.gettransaction(txid) + for detail in txdata["details"]: + if detail["category"] == "send": + assert detail["abandoned"] - # Should have been accepted by the mempool - assert txid in wallet.rpc.getrawmempool() - assert wallet.get_balance()["untrusted_pending"] == 0.0001 - finally: - # Clean up - bitcoind_controller.stop_bitcoind() + # Can we now spend those same inputs? + outputs = {wallet.getnewaddress(): 0.0001} + tx = wallet.rpc.createrawtransaction(inputs, outputs) + + # Fund this tx with a high enough fee + relayfee = satoshi_round(rpc.getnetworkinfo()["relayfee"]) + wallet.rpc.settxfee(str(relayfee * Decimal("3.0"))) + + txF = wallet.rpc.fundrawtransaction(tx) + wallet.rpc.settxfee(0) # return to automatic fee selection + txFS = device.sign_raw_tx(txF["hex"], wallet) + txid = wallet.rpc.sendrawtransaction(txFS["hex"]) + + # Should have been accepted by the mempool + assert txid in wallet.rpc.getrawmempool() + assert wallet.get_balance()["untrusted_pending"] == 0.0001 + + # Clean up + bitcoind_controller.stop_bitcoind()