1#!/usr/bin/env python3
2# Copyright (c) 2017-2019 The Bitcoin Core developers
3# Distributed under the MIT software license, see the accompanying
4# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5"""Class for bitcoind node under test"""
6
7import contextlib
8import decimal
9import errno
10from enum import Enum
11import http.client
12import json
13import logging
14import os
15import re
16import subprocess
17import tempfile
18import time
19import urllib.parse
20import collections
21import shlex
22import sys
23
24from .authproxy import JSONRPCException
25from .util import (
26    MAX_NODES,
27    append_config,
28    delete_cookie_file,
29    get_rpc_proxy,
30    rpc_url,
31    wait_until,
32    p2p_port,
33    EncodeDecimal,
34)
35from .qtum import convert_btc_address_to_qtum
36from .qtumconfig import ENABLE_REDUCED_BLOCK_TIME
37
38BITCOIND_PROC_WAIT_TIMEOUT = 60
39
40
41class FailedToStartError(Exception):
42    """Raised when a node fails to start correctly."""
43
44
45class ErrorMatch(Enum):
46    FULL_TEXT = 1
47    FULL_REGEX = 2
48    PARTIAL_REGEX = 3
49
50
51class TestNode():
52    """A class for representing a bitcoind node under test.
53
54    This class contains:
55
56    - state about the node (whether it's running, etc)
57    - a Python subprocess.Popen object representing the running process
58    - an RPC connection to the node
59    - one or more P2P connections to the node
60
61
62    To make things easier for the test writer, any unrecognised messages will
63    be dispatched to the RPC connection."""
64
65    def __init__(self, i, datadir, *, chain, rpchost, timewait, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, enable_wallet=False):
66        """
67        Kwargs:
68            start_perf (bool): If True, begin profiling the node with `perf` as soon as
69                the node starts.
70        """
71
72        self.index = i
73        self.datadir = datadir
74        self.bitcoinconf = os.path.join(self.datadir, "qtum.conf")
75        self.stdout_dir = os.path.join(self.datadir, "stdout")
76        self.stderr_dir = os.path.join(self.datadir, "stderr")
77        self.chain = chain
78        self.rpchost = rpchost
79        self.rpc_timeout = timewait
80        self.binary = bitcoind
81        self.coverage_dir = coverage_dir
82        self.cwd = cwd
83        self.enable_wallet = enable_wallet
84        if extra_conf is not None:
85            append_config(datadir, extra_conf)
86        # Most callers will just need to add extra args to the standard list below.
87        # For those callers that need more flexibility, they can just set the args property directly.
88        # Note that common args are set in the config file (see initialize_datadir)
89        self.extra_args = extra_args
90        self.version = version
91        # Configuration for logging is set as command-line args rather than in the bitcoin.conf file.
92        # This means that starting a bitcoind using the temp dir to debug a failed test won't
93        # spam debug.log.
94        self.args = [
95            self.binary,
96            "-datadir=" + self.datadir,
97            "-logtimemicros",
98            "-debug",
99            "-debugexclude=libevent",
100            "-debugexclude=leveldb",
101            "-uacomment=testnode%d" % i,
102        ]
103        if use_valgrind:
104            default_suppressions_file = os.path.join(
105                os.path.dirname(os.path.realpath(__file__)),
106                "..", "..", "..", "contrib", "valgrind.supp")
107            suppressions_file = os.getenv("VALGRIND_SUPPRESSIONS_FILE",
108                                          default_suppressions_file)
109            self.args = ["valgrind", "--suppressions={}".format(suppressions_file),
110                         "--gen-suppressions=all", "--exit-on-first-error=yes",
111                         "--error-exitcode=1", "--quiet"] + self.args
112
113        if self.version is None or self.version >= 190000:
114            self.args.append("-logthreadnames")
115
116        self.cli = TestNodeCLI(bitcoin_cli, self.datadir)
117        self.use_cli = use_cli
118        self.start_perf = start_perf
119
120        self.running = False
121        self.process = None
122        self.rpc_connected = False
123        self.rpc = None
124        self.url = None
125        self.log = logging.getLogger('TestFramework.node%d' % i)
126        self.cleanup_on_exit = True # Whether to kill the node when this object goes away
127        # Cache perf subprocesses here by their data output filename.
128        self.perf_subprocesses = {}
129
130        self.p2ps = []
131
132    AddressKeyPair = collections.namedtuple('AddressKeyPair', ['address', 'key'])
133    PRIV_KEYS = [
134            # address , privkey
135            AddressKeyPair(convert_btc_address_to_qtum('mjTkW3DjgyZck4KbiRusZsqTgaYTxdSz6z'), 'cVpF924EspNh8KjYsfhgY96mmxvT6DgdWiTYMtMjuM74hJaU5psW'),
136            AddressKeyPair(convert_btc_address_to_qtum('msX6jQXvxiNhx3Q62PKeLPrhrqZQdSimTg'), 'cUxsWyKyZ9MAQTaAhUQWJmBbSvHMwSmuv59KgxQV7oZQU3PXN3KE'),
137            AddressKeyPair(convert_btc_address_to_qtum('mnonCMyH9TmAsSj3M59DsbH8H63U3RKoFP'), 'cTrh7dkEAeJd6b3MRX9bZK8eRmNqVCMH3LSUkE3dSFDyzjU38QxK'),
138            AddressKeyPair(convert_btc_address_to_qtum('mqJupas8Dt2uestQDvV2NH3RU8uZh2dqQR'), 'cVuKKa7gbehEQvVq717hYcbE9Dqmq7KEBKqWgWrYBa2CKKrhtRim'),
139            AddressKeyPair(convert_btc_address_to_qtum('msYac7Rvd5ywm6pEmkjyxhbCDKqWsVeYws'), 'cQDCBuKcjanpXDpCqacNSjYfxeQj8G6CAtH1Dsk3cXyqLNC4RPuh'),
140            AddressKeyPair(convert_btc_address_to_qtum('n2rnuUnwLgXqf9kk2kjvVm8R5BZK1yxQBi'), 'cQakmfPSLSqKHyMFGwAqKHgWUiofJCagVGhiB4KCainaeCSxeyYq'),
141            AddressKeyPair(convert_btc_address_to_qtum('myzuPxRwsf3vvGzEuzPfK9Nf2RfwauwYe6'), 'cQMpDLJwA8DBe9NcQbdoSb1BhmFxVjWD5gRyrLZCtpuF9Zi3a9RK'),
142            AddressKeyPair(convert_btc_address_to_qtum('mumwTaMtbxEPUswmLBBN3vM9oGRtGBrys8'), 'cSXmRKXVcoouhNNVpcNKFfxsTsToY5pvB9DVsFksF1ENunTzRKsy'),
143            AddressKeyPair(convert_btc_address_to_qtum('mpV7aGShMkJCZgbW7F6iZgrvuPHjZjH9qg'), 'cSoXt6tm3pqy43UMabY6eUTmR3eSUYFtB2iNQDGgb3VUnRsQys2k'),
144            AddressKeyPair(convert_btc_address_to_qtum('mq4fBNdckGtvY2mijd9am7DRsbRB4KjUkf'), 'cN55daf1HotwBAgAKWVgDcoppmUNDtQSfb7XLutTLeAgVc3u8hik'),
145            AddressKeyPair(convert_btc_address_to_qtum('mpFAHDjX7KregM3rVotdXzQmkbwtbQEnZ6'), 'cT7qK7g1wkYEMvKowd2ZrX1E5f6JQ7TM246UfqbCiyF7kZhorpX3'),
146            AddressKeyPair(convert_btc_address_to_qtum('mzRe8QZMfGi58KyWCse2exxEFry2sfF2Y7'), 'cPiRWE8KMjTRxH1MWkPerhfoHFn5iHPWVK5aPqjW8NxmdwenFinJ'),
147    ]
148
149    def get_deterministic_priv_key(self):
150        """Return a deterministic priv key in base58, that only depends on the node's index"""
151        assert len(self.PRIV_KEYS) == MAX_NODES
152        return self.PRIV_KEYS[self.index]
153
154    def _node_msg(self, msg: str) -> str:
155        """Return a modified msg that identifies this node by its index as a debugging aid."""
156        return "[node %d] %s" % (self.index, msg)
157
158    def _raise_assertion_error(self, msg: str):
159        """Raise an AssertionError with msg modified to identify this node."""
160        raise AssertionError(self._node_msg(msg))
161
162    def __del__(self):
163        # Ensure that we don't leave any bitcoind processes lying around after
164        # the test ends
165        if self.process and self.cleanup_on_exit:
166            # Should only happen on test failure
167            # Avoid using logger, as that may have already been shutdown when
168            # this destructor is called.
169            print(self._node_msg("Cleaning up leftover process"))
170            self.process.kill()
171
172    def __getattr__(self, name):
173        """Dispatches any unrecognised messages to the RPC connection or a CLI instance."""
174        if self.use_cli:
175            return getattr(self.cli, name)
176        else:
177            assert self.rpc_connected and self.rpc is not None, self._node_msg("Error: no RPC connection")
178            return getattr(self.rpc, name)
179
180    def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, **kwargs):
181        """Start the node."""
182        if extra_args is None:
183            extra_args = self.extra_args
184
185        # Add a new stdout and stderr file each time bitcoind is started
186        if stderr is None:
187            stderr = tempfile.NamedTemporaryFile(dir=self.stderr_dir, delete=False)
188        if stdout is None:
189            stdout = tempfile.NamedTemporaryFile(dir=self.stdout_dir, delete=False)
190        self.stderr = stderr
191        self.stdout = stdout
192
193        if cwd is None:
194            cwd = self.cwd
195
196        # Delete any existing cookie file -- if such a file exists (eg due to
197        # unclean shutdown), it will get overwritten anyway by bitcoind, and
198        # potentially interfere with our attempt to authenticate
199        delete_cookie_file(self.datadir, self.chain)
200
201        # add environment variable LIBC_FATAL_STDERR_=1 so that libc errors are written to stderr and not the terminal
202        subp_env = dict(os.environ, LIBC_FATAL_STDERR_="1")
203
204        if self.enable_wallet and not any(arg.startswith('-staking=') for arg in extra_args):
205            extra_args.append('-staking=0')
206
207        if not ENABLE_REDUCED_BLOCK_TIME and not any(arg.startswith('-reduceblocktimeheight=') for arg in extra_args):
208            extra_args.append('-reduceblocktimeheight=10000000')
209
210        if self.enable_wallet and not any(arg.startswith('-offlinestakingheight=') for arg in extra_args):
211            extra_args.append('-offlinestakingheight=1')
212
213        # Disable the spam filter as it may interfere with come tests sending lots and lots of blocks
214        if not any(arg.startswith('-headerspamfilter') for arg in extra_args):
215            extra_args.append('-headerspamfilter=0')
216
217        self.process = subprocess.Popen(self.args + extra_args, env=subp_env, stdout=stdout, stderr=stderr, cwd=cwd, **kwargs)
218
219        self.running = True
220        self.log.debug("bitcoind started, waiting for RPC to come up")
221
222        if self.start_perf:
223            self._start_perf()
224
225    def wait_for_rpc_connection(self):
226        """Sets up an RPC connection to the bitcoind process. Returns False if unable to connect."""
227        # Poll at a rate of four times per second
228        poll_per_s = 4
229        for _ in range(poll_per_s * self.rpc_timeout):
230            if self.process.poll() is not None:
231                raise FailedToStartError(self._node_msg(
232                    'bitcoind exited with status {} during initialization'.format(self.process.returncode)))
233            try:
234                rpc = get_rpc_proxy(rpc_url(self.datadir, self.index, self.chain, self.rpchost), self.index, timeout=self.rpc_timeout, coveragedir=self.coverage_dir)
235                rpc.getblockcount()
236                # If the call to getblockcount() succeeds then the RPC connection is up
237                self.log.debug("RPC successfully started")
238                if self.use_cli:
239                    return
240                self.rpc = rpc
241                self.rpc_connected = True
242                self.url = self.rpc.url
243                return
244            except IOError as e:
245                if e.errno != errno.ECONNREFUSED:  # Port not yet open?
246                    raise  # unknown IO error
247            except JSONRPCException as e:  # Initialization phase
248                # -28 RPC in warmup
249                # -342 Service unavailable, RPC server started but is shutting down due to error
250                if e.error['code'] != -28 and e.error['code'] != -342:
251                    raise  # unknown JSON RPC exception
252            except ConnectionResetError:
253                # This might happen when the RPC server is in warmup, but shut down before the call to getblockcount
254                # succeeds. Try again to properly raise the FailedToStartError
255                pass
256            except ValueError as e:  # cookie file not found and no rpcuser or rpcassword. bitcoind still starting
257                if "No RPC credentials" not in str(e):
258                    raise
259            time.sleep(1.0 / poll_per_s)
260        self._raise_assertion_error("Unable to connect to bitcoind")
261
262    def generate(self, nblocks, maxtries=1000000):
263        self.log.debug("TestNode.generate() dispatches `generate` call to `generatetoaddress`")
264        return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries)
265
266    def get_wallet_rpc(self, wallet_name):
267        if self.use_cli:
268            return self.cli("-rpcwallet={}".format(wallet_name))
269        else:
270            assert self.rpc_connected and self.rpc, self._node_msg("RPC not connected")
271            wallet_path = "wallet/{}".format(urllib.parse.quote(wallet_name))
272            return self.rpc / wallet_path
273
274    def stop_node(self, expected_stderr='', wait=0):
275        """Stop the node."""
276        if not self.running:
277            return
278        self.log.debug("Stopping node")
279        try:
280            # Do not use wait argument when testing older nodes, e.g. in feature_backwards_compatibility.py
281            if self.version is None or self.version >= 180000:
282                self.stop(wait=wait)
283            else:
284                self.stop()
285        except http.client.CannotSendRequest:
286            self.log.exception("Unable to stop node.")
287
288        # If there are any running perf processes, stop them.
289        for profile_name in tuple(self.perf_subprocesses.keys()):
290            self._stop_perf(profile_name)
291
292        # Check that stderr is as expected
293        self.stderr.seek(0)
294        stderr = self.stderr.read().decode('utf-8').strip()
295        if stderr != expected_stderr:
296            raise AssertionError("Unexpected stderr {} != {}".format(stderr, expected_stderr))
297
298        self.stdout.close()
299        self.stderr.close()
300
301        del self.p2ps[:]
302
303    def is_node_stopped(self):
304        """Checks whether the node has stopped.
305
306        Returns True if the node has stopped. False otherwise.
307        This method is responsible for freeing resources (self.process)."""
308        if not self.running:
309            return True
310        return_code = self.process.poll()
311        if return_code is None:
312            return False
313
314        # process has stopped. Assert that it didn't return an error code.
315        assert return_code == 0, self._node_msg(
316            "Node returned non-zero exit code (%d) when stopping" % return_code)
317        self.running = False
318        self.process = None
319        self.rpc_connected = False
320        self.rpc = None
321        self.log.debug("Node stopped")
322        return True
323
324    def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT):
325        wait_until(self.is_node_stopped, timeout=timeout)
326
327    @contextlib.contextmanager
328    def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2):
329        if unexpected_msgs is None:
330            unexpected_msgs = []
331        time_end = time.time() + timeout
332        debug_log = os.path.join(self.datadir, self.chain, 'debug.log')
333        with open(debug_log, encoding='utf-8') as dl:
334            dl.seek(0, 2)
335            prev_size = dl.tell()
336
337        yield
338
339        while True:
340            found = True
341            with open(debug_log, encoding='utf-8') as dl:
342                dl.seek(prev_size)
343                log = dl.read()
344            print_log = " - " + "\n - ".join(log.splitlines())
345            for unexpected_msg in unexpected_msgs:
346                if re.search(re.escape(unexpected_msg), log, flags=re.MULTILINE):
347                    self._raise_assertion_error('Unexpected message "{}" partially matches log:\n\n{}\n\n'.format(unexpected_msg, print_log))
348            for expected_msg in expected_msgs:
349                if re.search(re.escape(expected_msg), log, flags=re.MULTILINE) is None:
350                    found = False
351            if found:
352                return
353            if time.time() >= time_end:
354                break
355            time.sleep(0.05)
356        self._raise_assertion_error('Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(str(expected_msgs), print_log))
357
358    @contextlib.contextmanager
359    def profile_with_perf(self, profile_name):
360        """
361        Context manager that allows easy profiling of node activity using `perf`.
362
363        See `test/functional/README.md` for details on perf usage.
364
365        Args:
366            profile_name (str): This string will be appended to the
367                profile data filename generated by perf.
368        """
369        subp = self._start_perf(profile_name)
370
371        yield
372
373        if subp:
374            self._stop_perf(profile_name)
375
376    def _start_perf(self, profile_name=None):
377        """Start a perf process to profile this node.
378
379        Returns the subprocess running perf."""
380        subp = None
381
382        def test_success(cmd):
383            return subprocess.call(
384                # shell=True required for pipe use below
385                cmd, shell=True,
386                stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) == 0
387
388        if not sys.platform.startswith('linux'):
389            self.log.warning("Can't profile with perf; only available on Linux platforms")
390            return None
391
392        if not test_success('which perf'):
393            self.log.warning("Can't profile with perf; must install perf-tools")
394            return None
395
396        if not test_success('readelf -S {} | grep .debug_str'.format(shlex.quote(self.binary))):
397            self.log.warning(
398                "perf output won't be very useful without debug symbols compiled into bitcoind")
399
400        output_path = tempfile.NamedTemporaryFile(
401            dir=self.datadir,
402            prefix="{}.perf.data.".format(profile_name or 'test'),
403            delete=False,
404        ).name
405
406        cmd = [
407            'perf', 'record',
408            '-g',                     # Record the callgraph.
409            '--call-graph', 'dwarf',  # Compatibility for gcc's --fomit-frame-pointer.
410            '-F', '101',              # Sampling frequency in Hz.
411            '-p', str(self.process.pid),
412            '-o', output_path,
413        ]
414        subp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
415        self.perf_subprocesses[profile_name] = subp
416
417        return subp
418
419    def _stop_perf(self, profile_name):
420        """Stop (and pop) a perf subprocess."""
421        subp = self.perf_subprocesses.pop(profile_name)
422        output_path = subp.args[subp.args.index('-o') + 1]
423
424        subp.terminate()
425        subp.wait(timeout=10)
426
427        stderr = subp.stderr.read().decode()
428        if 'Consider tweaking /proc/sys/kernel/perf_event_paranoid' in stderr:
429            self.log.warning(
430                "perf couldn't collect data! Try "
431                "'sudo sysctl -w kernel.perf_event_paranoid=-1'")
432        else:
433            report_cmd = "perf report -i {}".format(output_path)
434            self.log.info("See perf output by running '{}'".format(report_cmd))
435
436    def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, match=ErrorMatch.FULL_TEXT, *args, **kwargs):
437        """Attempt to start the node and expect it to raise an error.
438
439        extra_args: extra arguments to pass through to bitcoind
440        expected_msg: regex that stderr should match when bitcoind fails
441
442        Will throw if bitcoind starts without an error.
443        Will throw if an expected_msg is provided and it does not match bitcoind's stdout."""
444        with tempfile.NamedTemporaryFile(dir=self.stderr_dir, delete=False) as log_stderr, \
445             tempfile.NamedTemporaryFile(dir=self.stdout_dir, delete=False) as log_stdout:
446            try:
447                self.start(extra_args, stdout=log_stdout, stderr=log_stderr, *args, **kwargs)
448                self.wait_for_rpc_connection()
449                self.stop_node()
450                self.wait_until_stopped()
451            except FailedToStartError as e:
452                self.log.debug('bitcoind failed to start: %s', e)
453                self.running = False
454                self.process = None
455                # Check stderr for expected message
456                if expected_msg is not None:
457                    log_stderr.seek(0)
458                    stderr = log_stderr.read().decode('utf-8').strip()
459                    if match == ErrorMatch.PARTIAL_REGEX:
460                        if re.search(expected_msg, stderr, flags=re.MULTILINE) is None:
461                            self._raise_assertion_error(
462                                'Expected message "{}" does not partially match stderr:\n"{}"'.format(expected_msg, stderr))
463                    elif match == ErrorMatch.FULL_REGEX:
464                        if re.fullmatch(expected_msg, stderr) is None:
465                            self._raise_assertion_error(
466                                'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, stderr))
467                    elif match == ErrorMatch.FULL_TEXT:
468                        if expected_msg != stderr:
469                            self._raise_assertion_error(
470                                'Expected message "{}" does not fully match stderr:\n"{}"'.format(expected_msg, stderr))
471            else:
472                if expected_msg is None:
473                    assert_msg = "bitcoind should have exited with an error"
474                else:
475                    assert_msg = "bitcoind should have exited with expected error " + expected_msg
476                self._raise_assertion_error(assert_msg)
477
478    def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, **kwargs):
479        """Add a p2p connection to the node.
480
481        This method adds the p2p connection to the self.p2ps list and also
482        returns the connection to the caller."""
483        if 'dstport' not in kwargs:
484            kwargs['dstport'] = p2p_port(self.index)
485        if 'dstaddr' not in kwargs:
486            kwargs['dstaddr'] = '127.0.0.1'
487
488        p2p_conn.peer_connect(**kwargs, net=self.chain)()
489        self.p2ps.append(p2p_conn)
490        if wait_for_verack:
491            # Wait for the node to send us the version and verack
492            p2p_conn.wait_for_verack()
493            # At this point we have sent our version message and received the version and verack, however the full node
494            # has not yet received the verack from us (in reply to their version). So, the connection is not yet fully
495            # established (fSuccessfullyConnected).
496            #
497            # This shouldn't lead to any issues when sending messages, since the verack will be in-flight before the
498            # message we send. However, it might lead to races where we are expecting to receive a message. E.g. a
499            # transaction that will be added to the mempool as soon as we return here.
500            #
501            # So syncing here is redundant when we only want to send a message, but the cost is low (a few milliseconds)
502            # in comparision to the upside of making tests less fragile and unexpected intermittent errors less likely.
503            p2p_conn.sync_with_ping()
504
505        return p2p_conn
506
507    @property
508    def p2p(self):
509        """Return the first p2p connection
510
511        Convenience property - most tests only use a single p2p connection to each
512        node, so this saves having to write node.p2ps[0] many times."""
513        assert self.p2ps, self._node_msg("No p2p connection")
514        return self.p2ps[0]
515
516    def disconnect_p2ps(self):
517        """Close all p2p connections to the node."""
518        for p in self.p2ps:
519            p.peer_disconnect()
520        del self.p2ps[:]
521
522
523class TestNodeCLIAttr:
524    def __init__(self, cli, command):
525        self.cli = cli
526        self.command = command
527
528    def __call__(self, *args, **kwargs):
529        return self.cli.send_cli(self.command, *args, **kwargs)
530
531    def get_request(self, *args, **kwargs):
532        return lambda: self(*args, **kwargs)
533
534
535def arg_to_cli(arg):
536    if isinstance(arg, bool):
537        return str(arg).lower()
538    elif isinstance(arg, dict) or isinstance(arg, list):
539        return json.dumps(arg, default=EncodeDecimal)
540    else:
541        return str(arg)
542
543
544class TestNodeCLI():
545    """Interface to bitcoin-cli for an individual node"""
546
547    def __init__(self, binary, datadir):
548        self.options = []
549        self.binary = binary
550        self.datadir = datadir
551        self.input = None
552        self.log = logging.getLogger('TestFramework.bitcoincli')
553
554    def __call__(self, *options, input=None):
555        # TestNodeCLI is callable with bitcoin-cli command-line options
556        cli = TestNodeCLI(self.binary, self.datadir)
557        cli.options = [str(o) for o in options]
558        cli.input = input
559        return cli
560
561    def __getattr__(self, command):
562        return TestNodeCLIAttr(self, command)
563
564    def batch(self, requests):
565        results = []
566        for request in requests:
567            try:
568                results.append(dict(result=request()))
569            except JSONRPCException as e:
570                results.append(dict(error=e))
571        return results
572
573    def send_cli(self, command=None, *args, **kwargs):
574        """Run bitcoin-cli command. Deserializes returned string as python object."""
575        pos_args = [arg_to_cli(arg) for arg in args]
576        named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()]
577        assert not (pos_args and named_args), "Cannot use positional arguments and named arguments in the same bitcoin-cli call"
578        p_args = [self.binary, "-datadir=" + self.datadir] + self.options
579        if named_args:
580            p_args += ["-named"]
581        if command is not None:
582            p_args += [command]
583        p_args += pos_args + named_args
584        self.log.debug("Running bitcoin-cli command: %s" % command)
585        process = subprocess.Popen(p_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
586        cli_stdout, cli_stderr = process.communicate(input=self.input)
587        returncode = process.poll()
588        if returncode:
589            match = re.match(r'error code: ([-0-9]+)\nerror message:\n(.*)', cli_stderr)
590            if match:
591                code, message = match.groups()
592                raise JSONRPCException(dict(code=int(code), message=message))
593            # Ignore cli_stdout, raise with cli_stderr
594            raise subprocess.CalledProcessError(returncode, self.binary, output=cli_stderr)
595        try:
596            return json.loads(cli_stdout, parse_float=decimal.Decimal)
597        except json.JSONDecodeError:
598            return cli_stdout.rstrip("\n")
599