Skip to content

Commit 6fbe4b1

Browse files
moneymanolisKim Neunert
andauthored
Bugfix: Internal server error when node connection breaks down (#1920)
* BrokenCoreConnectionException added to handle lost RPC + some minor error handling fixes * clicking redirects to node config if there is no node connection * handling broken connection when there is no other error superseding (i.e. no wallets) * fix test results, property device_manager in user, bcce in get_rpc in node, improved rpc property in node * fix at least one test * fix for rest test * doc string for _get_rpc * fix for node test * marked test as slow in wallet managers test * safety checks in node controller * fix last test, revert deletion of raising exception Co-authored-by: Kim Neunert <[email protected]>
1 parent 392da54 commit 6fbe4b1

File tree

18 files changed

+302
-137
lines changed

18 files changed

+302
-137
lines changed

src/cryptoadvance/specter/api/rest/resource_psbt.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import logging
2+
import base64
3+
from json import dumps
24

35
from .base import (
46
SecureResource,
57
rest_resource,
68
)
7-
from flask import current_app as app, request
9+
10+
from flask import current_app as app, request, Response
811
from ...wallet import Wallet
912
from ...commands.psbt_creator import PsbtCreator
10-
13+
from ...specter_error import SpecterError
1114
from .. import auth
1215

1316
logger = logging.getLogger(__name__)
@@ -20,11 +23,16 @@ class ResourcePsbt(SecureResource):
2023
endpoints = ["/v1alpha/wallets/<wallet_alias>/psbt"]
2124

2225
def get(self, wallet_alias):
23-
# ToDo: check whether the user has access to the wallet
2426
user = auth.current_user()
25-
wallet: Wallet = app.specter.user_manager.get_user(
26-
user
27-
).wallet_manager.get_by_alias(wallet_alias)
27+
wallet_manager = app.specter.user_manager.get_user(user).wallet_manager
28+
# Check that the wallet belongs to the user from Basic Auth
29+
try:
30+
wallet = wallet_manager.get_by_alias(wallet_alias)
31+
except SpecterError:
32+
error_message = dumps(
33+
{"message": "The wallet does not belong to the user in the request."}
34+
)
35+
return Response(error_message, 403)
2836
pending_psbts = wallet.pending_psbts_dict()
2937
return {"result": pending_psbts or {}}
3038

src/cryptoadvance/specter/managers/wallet_manager.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from ..liquid.wallet import LWallet
1616
from ..persistence import delete_folder
1717
from ..rpc import RpcError, get_default_datadir
18-
from ..specter_error import SpecterError, handle_exception
18+
from ..specter_error import SpecterError, SpecterInternalException, handle_exception
1919
from ..wallet import ( # TODO: `purposes` unused here, but other files rely on this import
2020
Wallet,
2121
purposes,
@@ -69,7 +69,7 @@ def update(
6969
use_threading : for the _update method which is heavily communicating with Bitcoin Core
7070
"""
7171
if (chain is None and rpc is not None) or (chain is not None and rpc is None):
72-
raise Exception(
72+
raise SpecterInternalException(
7373
f"Chain ({chain}) and rpc ({rpc}) can only be changed with one another"
7474
)
7575
if self.is_loading:
@@ -89,7 +89,7 @@ def update(
8989
else:
9090
if rpc:
9191
logger.error(
92-
f"Prevented Trying to update wallet_Manager with broken {rpc}"
92+
f"Prevented trying to update Wallet Manager with broken {rpc}"
9393
)
9494
# wallets_update_list is something like:
9595
# {'Specter': {'name': 'Specter', 'alias': 'pacman', ... }, 'another_wallet': { ... } }
@@ -244,7 +244,10 @@ def get_by_alias(self, alias):
244244
for wallet_name in self.wallets:
245245
if self.wallets[wallet_name] and self.wallets[wallet_name].alias == alias:
246246
return self.wallets[wallet_name]
247-
raise SpecterError("Wallet %s does not exist!" % alias)
247+
raise SpecterError(
248+
"Wallet %s could not be loaded. Are you connected with Bitcoin Core?"
249+
% alias
250+
)
248251

249252
@property
250253
def failed_load_wallets(self) -> list:

src/cryptoadvance/specter/node.py

Lines changed: 66 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
autodetect_rpc_confs,
1515
get_default_datadir,
1616
)
17+
from .specter_error import BrokenCoreConnectionException
1718

1819
logger = logging.getLogger(__name__)
1920

@@ -74,7 +75,10 @@ def __init__(
7475
self.manager = manager
7576
self.proxy_url = manager.proxy_url
7677
self.only_tor = manager.only_tor
77-
self.rpc = self.get_rpc()
78+
try:
79+
self.rpc = self._get_rpc()
80+
except BrokenCoreConnectionException:
81+
self.rpc = None
7882
self._asset_labels = None
7983
self.check_info()
8084

@@ -128,26 +132,29 @@ def json(self):
128132
"node_type": self.node_type,
129133
}
130134

131-
def get_rpc(self):
132-
"""
133-
Checks if config have changed, compares with old rpc
134-
and returns new one if necessary
135-
"""
136-
if hasattr(self, "rpc"):
137-
rpc = self.rpc
135+
def _get_rpc(self):
136+
"""Checks if configurations have changed, compares with old rpc
137+
and returns new one if necessary.
138+
Aims to be exception safe, returns None if rpc is not working"""
139+
if hasattr(self, "_rpc"):
140+
rpc = self._rpc
138141
else:
139142
rpc = None
140143
if self.autodetect:
141-
if self.port:
142-
rpc_conf_arr = autodetect_rpc_confs(
143-
self.node_type,
144-
datadir=os.path.expanduser(self.datadir),
145-
port=self.port,
146-
)
147-
else:
148-
rpc_conf_arr = autodetect_rpc_confs(
149-
self.node_type, datadir=os.path.expanduser(self.datadir)
150-
)
144+
try:
145+
if self.port:
146+
# autodetect_rpc_confs is trying a RPC call
147+
rpc_conf_arr = autodetect_rpc_confs(
148+
self.node_type,
149+
datadir=os.path.expanduser(self.datadir),
150+
port=self.port,
151+
)
152+
else:
153+
rpc_conf_arr = autodetect_rpc_confs(
154+
self.node_type, datadir=os.path.expanduser(self.datadir)
155+
)
156+
except BrokenCoreConnectionException:
157+
return None
151158
if len(rpc_conf_arr) > 0:
152159
rpc = BitcoinRPC(
153160
**rpc_conf_arr[0], proxy_url=self.proxy_url, only_tor=self.only_tor
@@ -171,7 +178,7 @@ def get_rpc(self):
171178
)
172179

173180
if rpc == None:
174-
logger.error(f"connection results to None in get_rpc")
181+
logger.error(f"RPC connection is None in get_rpc. Returning None ...")
175182
return None
176183
# check if it's liquid
177184
try:
@@ -184,8 +191,8 @@ def get_rpc(self):
184191
return rpc # The user is failing to configure correctly
185192
logger.error(rpce)
186193
return None
187-
except ConnectionError as e:
188-
logger.error(f"{e} while get_rpc")
194+
except BrokenCoreConnectionException as bcce:
195+
logger.error(f"{bcce} while get_rpc")
189196
return None
190197
except Exception as e:
191198
logger.exception(e)
@@ -194,7 +201,7 @@ def get_rpc(self):
194201
return rpc
195202
else:
196203
logger.debug(
197-
f"connection {rpc} fails test_connection() returning None in get_rpc"
204+
f"Connection {rpc} fails test_connection() in get_rpc. Returning None ..."
198205
)
199206
return None
200207

@@ -239,10 +246,14 @@ def update_rpc(
239246
self.protocol = protocol
240247
update_rpc = True
241248
if update_rpc:
242-
self.rpc = self.get_rpc()
243-
if self.rpc and self.rpc.test_connection():
244-
logger.debug(f"persisting {self} in update_rpc")
245-
write_node(self, self.fullpath)
249+
try:
250+
self.rpc = self._get_rpc()
251+
if self.rpc and self.rpc.test_connection():
252+
logger.debug(f"persisting {self} in update_rpc")
253+
write_node(self, self.fullpath)
254+
except BrokenCoreConnectionException:
255+
self._mark_node_as_broken()
256+
return False
246257
self.check_info()
247258
return False if not self.rpc else self.rpc.test_connection()
248259

@@ -255,7 +266,6 @@ def rename(self, new_name):
255266

256267
def check_info(self):
257268
self._is_configured = self.rpc is not None
258-
self._is_running = False
259269
if self.rpc is not None and self.rpc.test_connection():
260270
try:
261271
res = [
@@ -289,12 +299,8 @@ def check_info(self):
289299
self.utxorescanwallet = None
290300
self._network_parameters = get_network(self.chain)
291301
self._is_running = True
292-
except Exception as e:
293-
self._info = {"chain": None}
294-
self._network_info = {"subversion": "", "version": 999999}
295-
self._network_parameters = get_network("main")
296-
logger.error(f"connection {self.rpc} could not suceed check_info")
297-
logger.exception("Exception %s while check_info()" % e)
302+
except BrokenCoreConnectionException:
303+
self._mark_node_as_broken()
298304
else:
299305
if self.rpc is None:
300306
logger.warning(f"connection of {self} is None in check_info")
@@ -315,24 +321,20 @@ def check_info(self):
315321
)
316322
except Exception as e:
317323
logger.exception(e)
318-
self._info = {"chain": None}
319-
self._network_info = {"subversion": "", "version": 999999}
320-
321-
if not self._is_running:
322-
self._info["chain"] = None
324+
self._mark_node_as_broken()
323325

324326
def test_rpc(self):
325327
"""tests the rpc-connection and returns a dict which helps
326328
to derive what might be wrong with the config
327329
ToDo: list an example here.
328330
"""
329-
rpc = self.get_rpc()
331+
rpc = self._get_rpc()
330332
if rpc is None:
331333
return {
332334
"out": "",
333335
"err": _("Connection to node failed"),
334336
"code": -1,
335-
"tests": {},
337+
"tests": {"connectable": False},
336338
}
337339
r = {}
338340
r["tests"] = {"connectable": False}
@@ -356,15 +358,14 @@ def test_rpc(self):
356358
r["err"] = "Wallets disabled"
357359

358360
r["out"] = json.dumps(rpc.getblockchaininfo(), indent=4)
359-
except ConnectionError as e:
360-
logger.info("Caught an ConnectionError while test_rpc: %s", e)
361-
361+
except BrokenCoreConnectionException as bcce:
362+
logger.info(f"Caught {bcce} while test_rpc")
362363
r["tests"]["connectable"] = False
363364
r["err"] = _("Failed to connect!")
364365
r["code"] = -1
365366
except RpcError as rpce:
366367
logger.info(
367-
f"Caught an RpcError while test_rpc status_code: {rpce.status_code} error_code:{rpce.error_code}"
368+
f"Caught an RpcError while test_rpc status_code: {rpce.status_code} error_code: {rpce.error_code}"
368369
)
369370
r["tests"]["connectable"] = True
370371
r["code"] = rpc.r.status_code
@@ -374,7 +375,7 @@ def test_rpc(self):
374375
else:
375376
r["err"] = str(rpce.status_code)
376377
except Exception as e:
377-
logger.error(
378+
logger.exception(
378379
"Caught an exception of type {} while test_rpc: {}".format(
379380
type(e), str(e)
380381
)
@@ -388,6 +389,16 @@ def test_rpc(self):
388389
r["code"] = -1
389390
return r
390391

392+
def _mark_node_as_broken(self):
393+
self._info = {"chain": None}
394+
self._network_info = {"subversion": "", "version": 999999}
395+
self._network_parameters = get_network("main")
396+
logger.debug(
397+
f"Node is not running, no RPC connection, check_info didn't succeed, setting RPC attribute to None ..."
398+
)
399+
self._info["chain"] = None
400+
self.rpc = None
401+
391402
def abortrescanutxo(self):
392403
"""use this to abort a rescan as it stores some state while rescanning"""
393404
self.rpc.scantxoutset("abort", [])
@@ -409,7 +420,11 @@ def is_liquid(self):
409420

410421
@property
411422
def is_running(self):
412-
return self._is_running
423+
if self._network_info["version"] == 999999:
424+
logger.debug(f"Node is not running")
425+
return False
426+
else:
427+
return True
413428

414429
@property
415430
def is_configured(self):
@@ -478,10 +493,12 @@ def is_liquid(self):
478493

479494
@property
480495
def rpc(self):
496+
"""Returns None if rpc is broken"""
481497
if not hasattr(self, "_rpc"):
482-
return None
483-
else:
484-
return self._rpc
498+
self._rpc = self._get_rpc()
499+
elif self._rpc is None:
500+
self._rpc = self._get_rpc()
501+
return self._rpc
485502

486503
@property
487504
def node_type(self):

src/cryptoadvance/specter/process_controller/node_controller.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919

2020
from ..helpers import load_jsons
2121
from ..rpc import BitcoinRPC, RpcError
22-
from ..specter_error import ExtProcTimeoutException, SpecterError
22+
from ..specter_error import (
23+
ExtProcTimeoutException,
24+
SpecterError,
25+
BrokenCoreConnectionException,
26+
)
2327
from ..util.shell import get_last_lines_from_file, which
2428

2529
logger = logging.getLogger(__name__)
@@ -272,11 +276,19 @@ def testcoin_faucet(self, address, amount=20, confirm_payment=True):
272276
def check_node(rpcconn, raise_exception=False):
273277
"""returns true if bitcoind is running on that address/port"""
274278
if raise_exception:
275-
rpcconn.get_rpc() # that call will also check the connection
276-
return True
279+
rpc = rpcconn.get_rpc() # that call will also check the connection
280+
if (
281+
rpc
282+
): # if-else is just in case, ideally, rpc should not be returned as None
283+
return True
284+
return False
277285
try:
278-
rpcconn.get_rpc() # that call will also check the connection
279-
return True
286+
rpc = rpcconn.get_rpc() # that call will also check the connection
287+
if (
288+
rpc
289+
): # if-else is just in case, ideally, rpc should not be returned as None
290+
return True
291+
return False
280292
except RpcError as e:
281293
# E.g. "Loading Index ..." #ToDo: check it here
282294
return False
@@ -288,10 +300,14 @@ def check_node(rpcconn, raise_exception=False):
288300
return False
289301
except NewConnectionError as e:
290302
return False
303+
except BrokenCoreConnectionException:
304+
return False
291305
except Exception as e:
292306
# We should avoid this:
293307
# If you see it in the logs, catch that new exception above
294-
logger.error("Unexpected Exception, THIS SHOULD NOT HAPPEN " + str(type(e)))
308+
logger.exception(
309+
"Unexpected Exception, THIS SHOULD NOT HAPPEN " + str(type(e))
310+
)
295311
logger.debug(f"could not reach bitcoind - message returned: {e}")
296312
return False
297313

src/cryptoadvance/specter/rpc.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import urllib3
1010

1111
from .helpers import is_ip_private
12-
from .specter_error import SpecterError, handle_exception
12+
from .specter_error import SpecterError, handle_exception, BrokenCoreConnectionException
13+
from urllib3.exceptions import NewConnectionError
14+
from requests.exceptions import ConnectionError
1315

1416
logger = logging.getLogger(__name__)
1517

@@ -366,6 +368,9 @@ def multi(self, calls: list, **kwargs):
366368
r = self.session.post(
367369
url, data=json.dumps(payload), headers=headers, timeout=timeout
368370
)
371+
except (ConnectionError, NewConnectionError, ConnectionRefusedError) as ce:
372+
raise BrokenCoreConnectionException()
373+
369374
except (requests.exceptions.Timeout, urllib3.exceptions.ReadTimeoutError) as to:
370375
# Timeout is effectively one of the two:
371376
# ConnectTimeout: The request timed out while trying to connect to the remote server

0 commit comments

Comments
 (0)