2017-08-15 23:34:07 +02:00
#!/usr/bin/env python3
# Copyright (c) 2017 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
2019-09-23 21:36:47 +02:00
""" Class for dashd node under test """
2017-08-15 23:34:07 +02:00
2017-08-24 22:01:16 +02:00
import decimal
2017-08-15 23:34:07 +02:00
import errno
import http . client
2017-08-24 22:01:16 +02:00
import json
2017-08-15 23:34:07 +02:00
import logging
import os
import subprocess
import time
2017-11-08 19:10:43 +01:00
from . authproxy import JSONRPCException
from . mininode import NodeConn
2017-08-15 23:34:07 +02:00
from . util import (
assert_equal ,
get_rpc_proxy ,
rpc_url ,
2017-09-06 20:02:08 +02:00
wait_until ,
2017-11-08 19:10:43 +01:00
p2p_port ,
2017-08-15 23:34:07 +02:00
)
2017-09-06 20:02:08 +02:00
BITCOIND_PROC_WAIT_TIMEOUT = 60
2017-08-15 23:34:07 +02:00
class TestNode ( ) :
2019-09-23 21:36:47 +02:00
""" A class for representing a dashd node under test.
2017-08-15 23:34:07 +02:00
This class contains :
- state about the node ( whether it ' s running, etc)
- a Python subprocess . Popen object representing the running process
- an RPC connection to the node
2017-11-08 19:10:43 +01:00
- one or more P2P connections to the node
2017-08-15 23:34:07 +02:00
2017-11-08 19:10:43 +01:00
To make things easier for the test writer , any unrecognised messages will
be dispatched to the RPC connection . """
2017-08-15 23:34:07 +02:00
def __init__ ( self , i , dirname , extra_args , rpchost , timewait , binary , stderr , mocktime , coverage_dir ) :
self . index = i
self . datadir = os . path . join ( dirname , " node " + str ( i ) )
self . rpchost = rpchost
2017-08-23 18:05:43 +02:00
if timewait :
self . rpc_timeout = timewait
else :
# Wait for up to 60 seconds for the RPC server to respond
self . rpc_timeout = 60
2017-08-15 23:34:07 +02:00
if binary is None :
2019-09-23 21:36:47 +02:00
self . binary = os . getenv ( " BITCOIND " , " dashd " )
2017-08-15 23:34:07 +02:00
else :
self . binary = binary
self . stderr = stderr
self . coverage_dir = coverage_dir
# Most callers will just need to add extra args to the standard list below. For those callers that need more flexibity, they can just set the args property directly.
self . extra_args = extra_args
self . args = [ self . binary , " -datadir= " + self . datadir , " -server " , " -keypool=1 " , " -discover=0 " , " -rest " , " -logtimemicros " , " -debug " , " -debugexclude=libevent " , " -debugexclude=leveldb " , " -mocktime= " + str ( mocktime ) , " -uacomment=testnode %d " % i ]
2019-09-24 01:09:01 +02:00
self . cli = TestNodeCLI ( os . getenv ( " BITCOINCLI " , " dash-cli " ) , self . datadir )
2017-08-24 22:01:16 +02:00
2019-09-23 21:43:21 +02:00
# Don't try auto backups (they fail a lot when running tests)
self . args . append ( " -createwalletbackups=0 " )
2017-08-15 23:34:07 +02:00
self . running = False
self . process = None
self . rpc_connected = False
self . rpc = None
self . url = None
self . log = logging . getLogger ( ' TestFramework.node %d ' % i )
2017-11-08 19:10:43 +01:00
self . p2ps = [ ]
def __getattr__ ( self , name ) :
2017-08-15 23:34:07 +02:00
""" Dispatches any unrecognised messages to the RPC connection. """
assert self . rpc_connected and self . rpc is not None , " Error: no RPC connection "
2017-11-08 19:10:43 +01:00
return getattr ( self . rpc , name )
2017-08-15 23:34:07 +02:00
2017-09-01 18:47:13 +02:00
def start ( self , extra_args = None , stderr = None ) :
2017-08-15 23:34:07 +02:00
""" Start the node. """
2017-09-01 18:47:13 +02:00
if extra_args is None :
extra_args = self . extra_args
if stderr is None :
stderr = self . stderr
self . process = subprocess . Popen ( self . args + extra_args , stderr = stderr )
2017-08-15 23:34:07 +02:00
self . running = True
2019-09-23 21:36:47 +02:00
self . log . debug ( " dashd started, waiting for RPC to come up " )
2017-08-15 23:34:07 +02:00
def wait_for_rpc_connection ( self ) :
2019-09-23 21:36:47 +02:00
""" Sets up an RPC connection to the dashd process. Returns False if unable to connect. """
2017-08-23 18:05:43 +02:00
# Poll at a rate of four times per second
poll_per_s = 4
for _ in range ( poll_per_s * self . rpc_timeout ) :
assert self . process . poll ( ) is None , " dashd exited with status %i during initialization " % self . process . returncode
2017-08-15 23:34:07 +02:00
try :
2017-09-01 18:47:13 +02:00
self . rpc = get_rpc_proxy ( rpc_url ( self . datadir , self . index , self . rpchost ) , self . index , timeout = self . rpc_timeout , coveragedir = self . coverage_dir )
2017-08-15 23:34:07 +02:00
self . rpc . getblockcount ( )
# If the call to getblockcount() succeeds then the RPC connection is up
self . rpc_connected = True
self . url = self . rpc . url
self . log . debug ( " RPC successfully started " )
return
except IOError as e :
if e . errno != errno . ECONNREFUSED : # Port not yet open?
raise # unknown IO error
except JSONRPCException as e : # Initialization phase
if e . error [ ' code ' ] != - 28 : # RPC in warmup?
raise # unknown JSON RPC exception
2019-09-23 21:36:47 +02:00
except ValueError as e : # cookie file not found and no rpcuser or rpcassword. dashd still starting
2017-08-15 23:34:07 +02:00
if " No RPC credentials " not in str ( e ) :
raise
2017-08-20 15:04:26 +02:00
time . sleep ( 1.0 / poll_per_s )
2019-09-23 21:36:47 +02:00
raise AssertionError ( " Unable to connect to dashd " )
2017-08-15 23:34:07 +02:00
def get_wallet_rpc ( self , wallet_name ) :
assert self . rpc_connected
assert self . rpc
wallet_path = " wallet/ %s " % wallet_name
return self . rpc / wallet_path
def stop_node ( self ) :
""" Stop the node. """
if not self . running :
return
self . log . debug ( " Stopping node " )
try :
self . stop ( )
except http . client . CannotSendRequest :
self . log . exception ( " Unable to stop node. " )
2017-11-08 19:10:43 +01:00
del self . p2ps [ : ]
2017-08-15 23:34:07 +02:00
def is_node_stopped ( self ) :
""" Checks whether the node has stopped.
Returns True if the node has stopped . False otherwise .
This method is responsible for freeing resources ( self . process ) . """
if not self . running :
return True
return_code = self . process . poll ( )
2017-09-06 20:02:08 +02:00
if return_code is None :
return False
# process has stopped. Assert that it didn't return an error code.
assert_equal ( return_code , 0 )
self . running = False
self . process = None
self . rpc_connected = False
self . rpc = None
self . log . debug ( " Node stopped " )
return True
def wait_until_stopped ( self , timeout = BITCOIND_PROC_WAIT_TIMEOUT ) :
wait_until ( self . is_node_stopped , timeout = timeout )
2017-08-15 23:34:07 +02:00
def node_encrypt_wallet ( self , passphrase ) :
""" " Encrypts the wallet.
2019-09-23 21:36:47 +02:00
This causes dashd to shutdown , so this method takes
2017-08-15 23:34:07 +02:00
care of cleaning up resources . """
self . encryptwallet ( passphrase )
2017-09-06 20:02:08 +02:00
self . wait_until_stopped ( )
2017-08-24 22:01:16 +02:00
2017-11-08 19:10:43 +01:00
def add_p2p_connection ( self , p2p_conn , * * kwargs ) :
""" Add a p2p connection to the node.
This method adds the p2p connection to the self . p2ps list and also
returns the connection to the caller . """
if ' dstport ' not in kwargs :
kwargs [ ' dstport ' ] = p2p_port ( self . index )
if ' dstaddr ' not in kwargs :
kwargs [ ' dstaddr ' ] = ' 127.0.0.1 '
self . p2ps . append ( p2p_conn )
kwargs . update ( { ' rpc ' : self . rpc , ' callback ' : p2p_conn } )
p2p_conn . add_connection ( NodeConn ( * * kwargs ) )
return p2p_conn
@property
def p2p ( self ) :
""" Return the first p2p connection
Convenience property - most tests only use a single p2p connection to each
node , so this saves having to write node . p2ps [ 0 ] many times . """
assert self . p2ps , " No p2p connection "
return self . p2ps [ 0 ]
def disconnect_p2p ( self , index = 0 ) :
""" Close the p2p connection to the node. """
# Connection could have already been closed by other end. Calling disconnect_p2p()
# on an already disconnected p2p connection is not an error.
if self . p2ps [ index ] . connection is not None :
self . p2ps [ index ] . connection . disconnect_node ( )
del self . p2ps [ index ]
2017-08-24 22:01:16 +02:00
class TestNodeCLI ( ) :
""" Interface to bitcoin-cli for an individual node """
def __init__ ( self , binary , datadir ) :
2017-09-06 18:07:21 +02:00
self . args = [ ]
2017-08-24 22:01:16 +02:00
self . binary = binary
self . datadir = datadir
2017-09-06 18:07:21 +02:00
self . input = None
def __call__ ( self , * args , input = None ) :
# TestNodeCLI is callable with bitcoin-cli command-line args
self . args = [ str ( arg ) for arg in args ]
self . input = input
return self
2017-08-24 22:01:16 +02:00
def __getattr__ ( self , command ) :
def dispatcher ( * args , * * kwargs ) :
return self . send_cli ( command , * args , * * kwargs )
return dispatcher
def send_cli ( self , command , * args , * * kwargs ) :
""" Run bitcoin-cli command. Deserializes returned string as python object. """
pos_args = [ str ( arg ) for arg in args ]
named_args = [ str ( key ) + " = " + str ( value ) for ( key , value ) in kwargs . items ( ) ]
assert not ( pos_args and named_args ) , " Cannot use positional arguments and named arguments in the same bitcoin-cli call "
2017-09-06 18:07:21 +02:00
p_args = [ self . binary , " -datadir= " + self . datadir ] + self . args
2017-08-24 22:01:16 +02:00
if named_args :
p_args + = [ " -named " ]
p_args + = [ command ] + pos_args + named_args
2017-09-06 17:36:13 +02:00
process = subprocess . Popen ( p_args , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , universal_newlines = True )
cli_stdout , cli_stderr = process . communicate ( input = self . input )
returncode = process . poll ( )
if returncode :
# Ignore cli_stdout, raise with cli_stderr
raise subprocess . CalledProcessError ( returncode , self . binary , output = cli_stderr )
return json . loads ( cli_stdout , parse_float = decimal . Decimal )