interface_bitcoin_cli.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2017-2022 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 """Test bitcoin-cli""" 6 7 from decimal import Decimal 8 import re 9 10 from test_framework.blocktools import COINBASE_MATURITY 11 from test_framework.test_framework import BitcoinTestFramework 12 from test_framework.util import ( 13 assert_equal, 14 assert_greater_than_or_equal, 15 assert_raises_process_error, 16 assert_raises_rpc_error, 17 get_auth_cookie, 18 ) 19 import time 20 21 # The block reward of coinbaseoutput.nValue (50) BTC/block matures after 22 # COINBASE_MATURITY (100) blocks. Therefore, after mining 101 blocks we expect 23 # node 0 to have a balance of (BLOCKS - COINBASE_MATURITY) * 50 BTC/block. 24 BLOCKS = COINBASE_MATURITY + 1 25 BALANCE = (BLOCKS - 100) * 50 26 27 JSON_PARSING_ERROR = 'error: Error parsing JSON: foo' 28 BLOCKS_VALUE_OF_ZERO = 'error: the first argument (number of blocks to generate, default: 1) must be an integer value greater than zero' 29 TOO_MANY_ARGS = 'error: too many arguments (maximum 2 for nblocks and maxtries)' 30 WALLET_NOT_LOADED = 'Requested wallet does not exist or is not loaded' 31 WALLET_NOT_SPECIFIED = 'Wallet file not specified' 32 33 34 def cli_get_info_string_to_dict(cli_get_info_string): 35 """Helper method to convert human-readable -getinfo into a dictionary""" 36 cli_get_info = {} 37 lines = cli_get_info_string.splitlines() 38 line_idx = 0 39 ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') 40 while line_idx < len(lines): 41 # Remove ansi colour code 42 line = ansi_escape.sub('', lines[line_idx]) 43 if "Balances" in line: 44 # When "Balances" appears in a line, all of the following lines contain "balance: wallet" until an empty line 45 cli_get_info["Balances"] = {} 46 while line_idx < len(lines) and not (lines[line_idx + 1] == ''): 47 line_idx += 1 48 balance, wallet = lines[line_idx].strip().split(" ") 49 # Remove right justification padding 50 wallet = wallet.strip() 51 if wallet == '""': 52 # Set default wallet("") to empty string 53 wallet = '' 54 cli_get_info["Balances"][wallet] = balance.strip() 55 elif ": " in line: 56 key, value = line.split(": ") 57 if key == 'Wallet' and value == '""': 58 # Set default wallet("") to empty string 59 value = '' 60 if key == "Proxies" and value == "n/a": 61 # Set N/A to empty string to represent no proxy 62 value = '' 63 cli_get_info[key.strip()] = value.strip() 64 line_idx += 1 65 return cli_get_info 66 67 68 class TestBitcoinCli(BitcoinTestFramework): 69 def add_options(self, parser): 70 self.add_wallet_options(parser) 71 72 def set_test_params(self): 73 self.setup_clean_chain = True 74 self.num_nodes = 1 75 76 def skip_test_if_missing_module(self): 77 self.skip_if_no_cli() 78 79 def run_test(self): 80 """Main test logic""" 81 self.generate(self.nodes[0], BLOCKS) 82 83 self.log.info("Compare responses from getblockchaininfo RPC and `bitcoin-cli getblockchaininfo`") 84 cli_response = self.nodes[0].cli.getblockchaininfo() 85 rpc_response = self.nodes[0].getblockchaininfo() 86 assert_equal(cli_response, rpc_response) 87 88 self.log.info("Test named arguments") 89 assert_equal(self.nodes[0].cli.echo(0, 1, arg3=3, arg5=5), ['0', '1', None, '3', None, '5']) 90 assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", self.nodes[0].cli.echo, 0, 1, arg1=1) 91 assert_raises_rpc_error(-8, "Parameter arg1 specified twice both as positional and named argument", self.nodes[0].cli.echo, 0, None, 2, arg1=1) 92 93 self.log.info("Test that later cli named arguments values silently overwrite earlier ones") 94 assert_equal(self.nodes[0].cli("-named", "echo", "arg0=0", "arg1=1", "arg2=2", "arg1=3").send_cli(), ['0', '3', '2']) 95 assert_raises_rpc_error(-8, "Parameter args specified multiple times", self.nodes[0].cli("-named", "echo", "args=[0,1,2,3]", "4", "5", "6", ).send_cli) 96 97 user, password = get_auth_cookie(self.nodes[0].datadir_path, self.chain) 98 99 self.log.info("Test -stdinrpcpass option") 100 assert_equal(BLOCKS, self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input=password).getblockcount()) 101 assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input='foo').echo) 102 103 self.log.info("Test -stdin and -stdinrpcpass") 104 assert_equal(['foo', 'bar'], self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input=f'{password}\nfoo\nbar').echo()) 105 assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input='foo').echo) 106 107 self.log.info("Test connecting to a non-existing server") 108 assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo) 109 110 self.log.info("Test connecting with non-existing RPC cookie file") 111 assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo) 112 113 self.log.info("Test -getinfo with arguments fails") 114 assert_raises_process_error(1, "-getinfo takes no arguments", self.nodes[0].cli('-getinfo').help) 115 116 self.log.info("Test -getinfo with -color=never does not return ANSI escape codes") 117 assert "\u001b[0m" not in self.nodes[0].cli('-getinfo', '-color=never').send_cli() 118 119 self.log.info("Test -getinfo with -color=always returns ANSI escape codes") 120 assert "\u001b[0m" in self.nodes[0].cli('-getinfo', '-color=always').send_cli() 121 122 self.log.info("Test -getinfo with invalid value for -color option") 123 assert_raises_process_error(1, "Invalid value for -color option. Valid values: always, auto, never.", self.nodes[0].cli('-getinfo', '-color=foo').send_cli) 124 125 self.log.info("Test -getinfo returns expected network and blockchain info") 126 if self.is_specified_wallet_compiled(): 127 self.import_deterministic_coinbase_privkeys() 128 self.nodes[0].encryptwallet(password) 129 cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() 130 cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) 131 132 network_info = self.nodes[0].getnetworkinfo() 133 blockchain_info = self.nodes[0].getblockchaininfo() 134 assert_equal(int(cli_get_info['Version']), network_info['version']) 135 assert_equal(cli_get_info['Verification progress'], "%.4f%%" % (blockchain_info['verificationprogress'] * 100)) 136 assert_equal(int(cli_get_info['Blocks']), blockchain_info['blocks']) 137 assert_equal(int(cli_get_info['Headers']), blockchain_info['headers']) 138 assert_equal(int(cli_get_info['Time offset (s)']), network_info['timeoffset']) 139 expected_network_info = f"in {network_info['connections_in']}, out {network_info['connections_out']}, total {network_info['connections']}" 140 assert_equal(cli_get_info["Network"], expected_network_info) 141 assert_equal(cli_get_info['Proxies'], network_info['networks'][0]['proxy']) 142 assert_equal(Decimal(cli_get_info['Difficulty']), blockchain_info['difficulty']) 143 assert_equal(cli_get_info['Chain'], blockchain_info['chain']) 144 145 self.log.info("Test -getinfo and bitcoin-cli return all proxies") 146 self.restart_node(0, extra_args=["-proxy=127.0.0.1:9050", "-i2psam=127.0.0.1:7656"]) 147 network_info = self.nodes[0].getnetworkinfo() 148 cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() 149 cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) 150 assert_equal(cli_get_info["Proxies"], "127.0.0.1:9050 (ipv4, ipv6, onion, cjdns), 127.0.0.1:7656 (i2p)") 151 152 if self.is_specified_wallet_compiled(): 153 self.log.info("Test -getinfo and bitcoin-cli getwalletinfo return expected wallet info") 154 # Explicitly set the output type in order to have consistent tx vsize / fees 155 # for both legacy and descriptor wallets (disables the change address type detection algorithm) 156 self.restart_node(0, extra_args=["-addresstype=bech32", "-changetype=bech32"]) 157 assert_equal(Decimal(cli_get_info['Balance']), BALANCE) 158 assert 'Balances' not in cli_get_info_string 159 wallet_info = self.nodes[0].getwalletinfo() 160 assert_equal(int(cli_get_info['Keypool size']), wallet_info['keypoolsize']) 161 assert_equal(int(cli_get_info['Unlocked until']), wallet_info['unlocked_until']) 162 assert_equal(Decimal(cli_get_info['Transaction fee rate (-paytxfee) (BTC/kvB)']), wallet_info['paytxfee']) 163 assert_equal(Decimal(cli_get_info['Min tx relay fee rate (BTC/kvB)']), network_info['relayfee']) 164 assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) 165 166 # Setup to test -getinfo, -generate, and -rpcwallet= with multiple wallets. 167 wallets = [self.default_wallet_name, 'Encrypted', 'secret'] 168 amounts = [BALANCE + Decimal('9.999928'), Decimal(9), Decimal(31)] 169 self.nodes[0].createwallet(wallet_name=wallets[1]) 170 self.nodes[0].createwallet(wallet_name=wallets[2]) 171 w1 = self.nodes[0].get_wallet_rpc(wallets[0]) 172 w2 = self.nodes[0].get_wallet_rpc(wallets[1]) 173 w3 = self.nodes[0].get_wallet_rpc(wallets[2]) 174 rpcwallet2 = f'-rpcwallet={wallets[1]}' 175 rpcwallet3 = f'-rpcwallet={wallets[2]}' 176 w1.walletpassphrase(password, self.rpc_timeout) 177 w2.encryptwallet(password) 178 w1.sendtoaddress(w2.getnewaddress(), amounts[1]) 179 w1.sendtoaddress(w3.getnewaddress(), amounts[2]) 180 181 # Mine a block to confirm; adds a block reward (50 BTC) to the default wallet. 182 self.generate(self.nodes[0], 1) 183 184 self.log.info("Test -getinfo with multiple wallets and -rpcwallet returns specified wallet balance") 185 for i in range(len(wallets)): 186 cli_get_info_string = self.nodes[0].cli('-getinfo', f'-rpcwallet={wallets[i]}').send_cli() 187 cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) 188 assert 'Balances' not in cli_get_info_string 189 assert_equal(cli_get_info["Wallet"], wallets[i]) 190 assert_equal(Decimal(cli_get_info['Balance']), amounts[i]) 191 192 self.log.info("Test -getinfo with multiple wallets and -rpcwallet=non-existing-wallet returns no balances") 193 cli_get_info_string = self.nodes[0].cli('-getinfo', '-rpcwallet=does-not-exist').send_cli() 194 assert 'Balance' not in cli_get_info_string 195 assert 'Balances' not in cli_get_info_string 196 197 self.log.info("Test -getinfo with multiple wallets returns all loaded wallet names and balances") 198 assert_equal(set(self.nodes[0].listwallets()), set(wallets)) 199 cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() 200 cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) 201 assert 'Balance' not in cli_get_info 202 for k, v in zip(wallets, amounts): 203 assert_equal(Decimal(cli_get_info['Balances'][k]), v) 204 205 # Unload the default wallet and re-verify. 206 self.nodes[0].unloadwallet(wallets[0]) 207 assert wallets[0] not in self.nodes[0].listwallets() 208 cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() 209 cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) 210 assert 'Balance' not in cli_get_info 211 assert 'Balances' in cli_get_info_string 212 for k, v in zip(wallets[1:], amounts[1:]): 213 assert_equal(Decimal(cli_get_info['Balances'][k]), v) 214 assert wallets[0] not in cli_get_info 215 216 self.log.info("Test -getinfo after unloading all wallets except a non-default one returns its balance") 217 self.nodes[0].unloadwallet(wallets[2]) 218 assert_equal(self.nodes[0].listwallets(), [wallets[1]]) 219 cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() 220 cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) 221 assert 'Balances' not in cli_get_info_string 222 assert_equal(cli_get_info['Wallet'], wallets[1]) 223 assert_equal(Decimal(cli_get_info['Balance']), amounts[1]) 224 225 self.log.info("Test -getinfo with -rpcwallet=remaining-non-default-wallet returns only its balance") 226 cli_get_info_string = self.nodes[0].cli('-getinfo', rpcwallet2).send_cli() 227 cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) 228 assert 'Balances' not in cli_get_info_string 229 assert_equal(cli_get_info['Wallet'], wallets[1]) 230 assert_equal(Decimal(cli_get_info['Balance']), amounts[1]) 231 232 self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balances") 233 cli_get_info_string = self.nodes[0].cli('-getinfo', rpcwallet3).send_cli() 234 cli_get_info_keys = cli_get_info_string_to_dict(cli_get_info_string) 235 assert 'Balance' not in cli_get_info_keys 236 assert 'Balances' not in cli_get_info_string 237 238 # Test bitcoin-cli -generate. 239 n1 = 3 240 n2 = 4 241 w2.walletpassphrase(password, self.rpc_timeout) 242 blocks = self.nodes[0].getblockcount() 243 244 self.log.info('Test -generate with no args') 245 generate = self.nodes[0].cli('-generate').send_cli() 246 assert_equal(set(generate.keys()), {'address', 'blocks'}) 247 assert_equal(len(generate["blocks"]), 1) 248 assert_equal(self.nodes[0].getblockcount(), blocks + 1) 249 250 self.log.info('Test -generate with bad args') 251 assert_raises_process_error(1, JSON_PARSING_ERROR, self.nodes[0].cli('-generate', 'foo').echo) 252 assert_raises_process_error(1, BLOCKS_VALUE_OF_ZERO, self.nodes[0].cli('-generate', 0).echo) 253 assert_raises_process_error(1, TOO_MANY_ARGS, self.nodes[0].cli('-generate', 1, 2, 3).echo) 254 255 self.log.info('Test -generate with nblocks') 256 generate = self.nodes[0].cli('-generate', n1).send_cli() 257 assert_equal(set(generate.keys()), {'address', 'blocks'}) 258 assert_equal(len(generate["blocks"]), n1) 259 assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n1) 260 261 self.log.info('Test -generate with nblocks and maxtries') 262 generate = self.nodes[0].cli('-generate', n2, 1000000).send_cli() 263 assert_equal(set(generate.keys()), {'address', 'blocks'}) 264 assert_equal(len(generate["blocks"]), n2) 265 assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n1 + n2) 266 267 self.log.info('Test -generate -rpcwallet in single-wallet mode') 268 generate = self.nodes[0].cli(rpcwallet2, '-generate').send_cli() 269 assert_equal(set(generate.keys()), {'address', 'blocks'}) 270 assert_equal(len(generate["blocks"]), 1) 271 assert_equal(self.nodes[0].getblockcount(), blocks + 2 + n1 + n2) 272 273 self.log.info('Test -generate -rpcwallet=unloaded wallet raises RPC error') 274 assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate').echo) 275 assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate', 'foo').echo) 276 assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate', 0).echo) 277 assert_raises_rpc_error(-18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, '-generate', 1, 2, 3).echo) 278 279 # Test bitcoin-cli -generate with -rpcwallet in multiwallet mode. 280 self.nodes[0].loadwallet(wallets[2]) 281 n3 = 4 282 n4 = 10 283 blocks = self.nodes[0].getblockcount() 284 285 self.log.info('Test -generate -rpcwallet with no args') 286 generate = self.nodes[0].cli(rpcwallet2, '-generate').send_cli() 287 assert_equal(set(generate.keys()), {'address', 'blocks'}) 288 assert_equal(len(generate["blocks"]), 1) 289 assert_equal(self.nodes[0].getblockcount(), blocks + 1) 290 291 self.log.info('Test -generate -rpcwallet with bad args') 292 assert_raises_process_error(1, JSON_PARSING_ERROR, self.nodes[0].cli(rpcwallet2, '-generate', 'foo').echo) 293 assert_raises_process_error(1, BLOCKS_VALUE_OF_ZERO, self.nodes[0].cli(rpcwallet2, '-generate', 0).echo) 294 assert_raises_process_error(1, TOO_MANY_ARGS, self.nodes[0].cli(rpcwallet2, '-generate', 1, 2, 3).echo) 295 296 self.log.info('Test -generate -rpcwallet with nblocks') 297 generate = self.nodes[0].cli(rpcwallet2, '-generate', n3).send_cli() 298 assert_equal(set(generate.keys()), {'address', 'blocks'}) 299 assert_equal(len(generate["blocks"]), n3) 300 assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n3) 301 302 self.log.info('Test -generate -rpcwallet with nblocks and maxtries') 303 generate = self.nodes[0].cli(rpcwallet2, '-generate', n4, 1000000).send_cli() 304 assert_equal(set(generate.keys()), {'address', 'blocks'}) 305 assert_equal(len(generate["blocks"]), n4) 306 assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n3 + n4) 307 308 self.log.info('Test -generate without -rpcwallet in multiwallet mode raises RPC error') 309 assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate').echo) 310 assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate', 'foo').echo) 311 assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate', 0).echo) 312 assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, self.nodes[0].cli('-generate', 1, 2, 3).echo) 313 else: 314 self.log.info("*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") 315 self.generate(self.nodes[0], 25) # maintain block parity with the wallet_compiled conditional branch 316 317 self.log.info("Test -version with node stopped") 318 self.stop_node(0) 319 cli_response = self.nodes[0].cli('-version').send_cli() 320 assert f"{self.config['environment']['PACKAGE_NAME']} RPC client version" in cli_response 321 322 self.log.info("Test -rpcwait option successfully waits for RPC connection") 323 self.nodes[0].start() # start node without RPC connection 324 self.nodes[0].wait_for_cookie_credentials() # ensure cookie file is available to avoid race condition 325 blocks = self.nodes[0].cli('-rpcwait').send_cli('getblockcount') 326 self.nodes[0].wait_for_rpc_connection() 327 assert_equal(blocks, BLOCKS + 25) 328 329 self.log.info("Test -rpcwait option waits at most -rpcwaittimeout seconds for startup") 330 self.stop_node(0) # stop the node so we time out 331 start_time = time.time() 332 assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcwait', '-rpcwaittimeout=5').echo) 333 assert_greater_than_or_equal(time.time(), start_time + 5) 334 335 336 if __name__ == '__main__': 337 TestBitcoinCli().main()