Chore: Ci improvements (#103) (#1403)

* Ci improvements (#103)

* pin bitcoin to new v0.21.1 tag

* fix binary installation

* for binary install, pytest.ini is the file to use

* kick

* adjusted Dockerfile to have at least wget to download artifacts

* investigate test_hwibridge timing out

* restest with v0.20.1

* implements timeout, hope that was reason to hang

* remove livelogging again

* fix hanging test

* Enable bitcoind-log-output on demand

* Timeouts now raising SpecterErrors

* catch a bit more generic Exception

* increase timeout

* fix abandontx test

* simplified code

* increase timeout

* increased timeout also for pytest

* separate timeouts for liquid/bitcoin

* forget checkin

* increase timeout for bitcoin as well

* bugfix: node specific timeouts

* increase timeout for elm hotwallet

* Now timeouts for all occurences of broadcasting

* global vars done right

* Yet another ReadTimeoutError to take account for

* fix assertion

* fix not-defined issue

* proper status_code checking instead of error_code

* increase wait-time

* increase btc:mine wait to 15 s

Co-authored-by: Stepan Snigirev <snigirev.stepan@gmail.com>

* rollback to v0.20.1

* rollback - it is a src-install, stupid

* mixed up bitcoin and elements

* forgot to fix reference

* rollback test_abandon_purged_tx

* increase timeout to 30 s

* increase Liquid on cypress to 40s

Co-authored-by: Stepan Snigirev <snigirev.stepan@gmail.com>
This commit is contained in:
Kim Neunert 2021-09-19 22:33:13 +02:00 committed by GitHub
parent b4e3e2689e
commit cb8968b5fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 305 additions and 184 deletions

View file

@ -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

View file

@ -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

View file

@ -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"
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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 =

View file

@ -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):

View file

@ -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,

View file

@ -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:

View file

@ -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)")

View file

@ -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")

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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")

View file

@ -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
)

View file

@ -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()