interface_rpc.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2018-present 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  """Tests some generic aspects of the RPC interface."""
   6  
   7  import json
   8  import os
   9  from dataclasses import dataclass
  10  from test_framework.test_framework import BitcoinTestFramework
  11  from test_framework.util import assert_equal, assert_greater_than_or_equal
  12  from threading import Thread
  13  from typing import Optional
  14  import subprocess
  15  
  16  
  17  RPC_INVALID_PARAMETER      = -8
  18  RPC_METHOD_NOT_FOUND       = -32601
  19  RPC_INVALID_REQUEST        = -32600
  20  RPC_PARSE_ERROR            = -32700
  21  
  22  
  23  @dataclass
  24  class BatchOptions:
  25      version: Optional[int] = None
  26      notification: bool = False
  27      request_fields: Optional[dict] = None
  28      response_fields: Optional[dict] = None
  29  
  30  
  31  def format_request(options, idx, fields):
  32      request = {}
  33      if options.version == 1:
  34          request.update(version="1.1")
  35      elif options.version == 2:
  36          request.update(jsonrpc="2.0")
  37      elif options.version is not None:
  38          raise NotImplementedError(f"Unknown JSONRPC version {options.version}")
  39      if not options.notification:
  40          request.update(id=idx)
  41      request.update(fields)
  42      if options.request_fields:
  43          request.update(options.request_fields)
  44      return request
  45  
  46  
  47  def format_response(options, idx, fields):
  48      if options.version == 2 and options.notification:
  49          return None
  50      response = {}
  51      if not options.notification:
  52          response.update(id=idx)
  53      if options.version == 2:
  54          response.update(jsonrpc="2.0")
  55      else:
  56          response.update(result=None, error=None)
  57      response.update(fields)
  58      if options.response_fields:
  59          response.update(options.response_fields)
  60      return response
  61  
  62  
  63  def send_raw_rpc(node, raw_body: bytes) -> tuple[object, int]:
  64      return node._request("POST", "/", raw_body)
  65  
  66  
  67  def send_json_rpc(node, body: object) -> tuple[object, int]:
  68      raw = json.dumps(body).encode("utf-8")
  69      return send_raw_rpc(node, raw)
  70  
  71  
  72  def expect_http_rpc_status(expected_http_status, expected_rpc_error_code, node, method, params, version=1, notification=False):
  73      req = format_request(BatchOptions(version, notification), 0, {"method": method, "params": params})
  74      response, status = send_json_rpc(node, req)
  75  
  76      if expected_rpc_error_code is not None:
  77          assert_equal(response["error"]["code"], expected_rpc_error_code)
  78  
  79      assert_equal(status, expected_http_status)
  80  
  81  
  82  def test_work_queue_getblock(node, got_exceeded_error):
  83      while not got_exceeded_error:
  84          try:
  85              node.cli("waitfornewblock", "500").send_cli()
  86          except subprocess.CalledProcessError as e:
  87              assert_equal(e.output, 'error: Server response: Work queue depth exceeded\n')
  88              got_exceeded_error.append(True)
  89  
  90  
  91  class RPCInterfaceTest(BitcoinTestFramework):
  92      def set_test_params(self):
  93          self.num_nodes = 1
  94          self.setup_clean_chain = True
  95          self.supports_cli = False
  96  
  97      def test_getrpcinfo(self):
  98          self.log.info("Testing getrpcinfo...")
  99  
 100          info = self.nodes[0].getrpcinfo()
 101          assert_equal(len(info['active_commands']), 1)
 102  
 103          command = info['active_commands'][0]
 104          assert_equal(command['method'], 'getrpcinfo')
 105          assert_greater_than_or_equal(command['duration'], 0)
 106          assert_equal(info['logpath'], os.path.join(self.nodes[0].chain_path, 'debug.log'))
 107  
 108      def test_batch_request(self, call_options):
 109          calls = [
 110              # A basic request that will work fine.
 111              {"method": "getblockcount"},
 112              # Request that will fail.  The whole batch request should still
 113              # work fine.
 114              {"method": "invalidmethod"},
 115              # Another call that should succeed.
 116              {"method": "getblockhash", "params": [0]},
 117              # Invalid request format
 118              {"pizza": "sausage"}
 119          ]
 120          results = [
 121              {"result": 0},
 122              {"error": {"code": RPC_METHOD_NOT_FOUND, "message": "Method not found"}},
 123              {"result": "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206"},
 124              {"error": {"code": RPC_INVALID_REQUEST, "message": "Missing method"}},
 125          ]
 126  
 127          request = []
 128          response = []
 129          for idx, (call, result) in enumerate(zip(calls, results), 1):
 130              options = call_options(idx)
 131              if options is None:
 132                  continue
 133              request.append(format_request(options, idx, call))
 134              r = format_response(options, idx, result)
 135              if r is not None:
 136                  response.append(r)
 137  
 138          rpc_response, http_status = send_json_rpc(self.nodes[0], request)
 139          if len(response) == 0 and len(request) > 0:
 140              assert_equal(http_status, 204)
 141              assert_equal(rpc_response, None)
 142          else:
 143              assert_equal(http_status, 200)
 144              assert_equal(rpc_response, response)
 145  
 146      def test_batch_requests(self):
 147          self.log.info("Testing empty batch request...")
 148          self.test_batch_request(lambda idx: None)
 149  
 150          self.log.info("Testing basic JSON-RPC 2.0 batch request...")
 151          self.test_batch_request(lambda idx: BatchOptions(version=2))
 152  
 153          self.log.info("Testing JSON-RPC 2.0 batch with notifications...")
 154          self.test_batch_request(lambda idx: BatchOptions(version=2, notification=idx < 2))
 155  
 156          self.log.info("Testing JSON-RPC 2.0 batch of ALL notifications...")
 157          self.test_batch_request(lambda idx: BatchOptions(version=2, notification=True))
 158  
 159          # JSONRPC 1.1 does not support batch requests, but test them for backwards compatibility.
 160          self.log.info("Testing nonstandard JSON-RPC 1.1 batch request...")
 161          self.test_batch_request(lambda idx: BatchOptions(version=1))
 162  
 163          self.log.info("Testing nonstandard mixed JSON-RPC 1.1/2.0 batch request...")
 164          self.test_batch_request(lambda idx: BatchOptions(version=2 if idx % 2 else 1))
 165  
 166          self.log.info("Testing nonstandard batch request without version numbers...")
 167          self.test_batch_request(lambda idx: BatchOptions())
 168  
 169          self.log.info("Testing nonstandard batch request without version numbers or ids...")
 170          self.test_batch_request(lambda idx: BatchOptions(notification=True))
 171  
 172          self.log.info("Testing nonstandard jsonrpc 1.0 version number is accepted...")
 173          self.test_batch_request(lambda idx: BatchOptions(request_fields={"jsonrpc": "1.0"}))
 174  
 175          self.log.info("Testing unrecognized jsonrpc version number is rejected...")
 176          self.test_batch_request(lambda idx: BatchOptions(
 177              request_fields={"jsonrpc": "2.1"},
 178              response_fields={"result": None, "error": {"code": RPC_INVALID_REQUEST, "message": "JSON-RPC version not supported"}}))
 179  
 180      def test_http_status_codes(self):
 181          self.log.info("Testing HTTP status codes for JSON-RPC 1.1 requests...")
 182          # OK
 183          expect_http_rpc_status(200, None,                  self.nodes[0], "getblockhash", [0])
 184          # Errors
 185          expect_http_rpc_status(404, RPC_METHOD_NOT_FOUND,  self.nodes[0], "invalidmethod", [])
 186          expect_http_rpc_status(500, RPC_INVALID_PARAMETER, self.nodes[0], "getblockhash", [42])
 187          # force-send empty request
 188          response, status = send_raw_rpc(self.nodes[0], b"")
 189          assert_equal(response, {"id": None, "result": None, "error": {"code": RPC_PARSE_ERROR, "message": "Parse error"}})
 190          assert_equal(status, 500)
 191          # force-send invalidly formatted request
 192          response, status = send_raw_rpc(self.nodes[0], b"this is bad")
 193          assert_equal(response, {"id": None, "result": None, "error": {"code": RPC_PARSE_ERROR, "message": "Parse error"}})
 194          assert_equal(status, 500)
 195  
 196          self.log.info("Testing HTTP status codes for JSON-RPC 2.0 requests...")
 197          # OK
 198          expect_http_rpc_status(200, None,                   self.nodes[0], "getblockhash", [0],  2, False)
 199          # RPC errors but not HTTP errors
 200          expect_http_rpc_status(200, RPC_METHOD_NOT_FOUND,   self.nodes[0], "invalidmethod", [],  2, False)
 201          expect_http_rpc_status(200, RPC_INVALID_PARAMETER,  self.nodes[0], "getblockhash", [42], 2, False)
 202          # force-send invalidly formatted requests
 203          response, status = send_json_rpc(self.nodes[0], {"jsonrpc": 2, "method": "getblockcount"})
 204          assert_equal(response, {"result": None, "error": {"code": RPC_INVALID_REQUEST, "message": "jsonrpc field must be a string"}})
 205          assert_equal(status, 400)
 206          response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "3.0", "method": "getblockcount"})
 207          assert_equal(response, {"result": None, "error": {"code": RPC_INVALID_REQUEST, "message": "JSON-RPC version not supported"}})
 208          assert_equal(status, 400)
 209  
 210          self.log.info("Testing HTTP status codes for JSON-RPC 2.0 notifications...")
 211          # Not notification: id exists
 212          response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "2.0", "id": None, "method": "getblockcount"})
 213          assert_equal(response["result"], 0)
 214          assert_equal(status, 200)
 215          # Not notification: JSON 1.1
 216          expect_http_rpc_status(200, None,                   self.nodes[0], "getblockcount", [],  1)
 217          # Not notification: has "id" field
 218          expect_http_rpc_status(200, None,                   self.nodes[0], "getblockcount", [],  2, False)
 219          block_count = self.nodes[0].getblockcount()
 220          # Notification response status code: HTTP_NO_CONTENT
 221          expect_http_rpc_status(204, None,                   self.nodes[0], "generatetoaddress", [1, "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202"],  2, True)
 222          # The command worked even though there was no response
 223          assert_equal(block_count + 1, self.nodes[0].getblockcount())
 224          # No error response for notifications even if they are invalid
 225          expect_http_rpc_status(204, None, self.nodes[0], "generatetoaddress", [1, "invalid_address"], 2, True)
 226          # Sanity check: command was not executed
 227          assert_equal(block_count + 1, self.nodes[0].getblockcount())
 228  
 229      def test_work_queue_exceeded(self):
 230          self.log.info("Testing work queue exceeded...")
 231          self.restart_node(0, ['-rpcworkqueue=1', '-rpcthreads=1'])
 232          got_exceeded_error = []
 233          threads = []
 234          for _ in range(3):
 235              t = Thread(target=test_work_queue_getblock, args=(self.nodes[0], got_exceeded_error))
 236              t.start()
 237              threads.append(t)
 238          for t in threads:
 239              t.join()
 240  
 241      def run_test(self):
 242          self.test_getrpcinfo()
 243          self.test_batch_requests()
 244          self.test_http_status_codes()
 245          self.test_work_queue_exceeded()
 246  
 247  
 248  if __name__ == '__main__':
 249      RPCInterfaceTest(__file__).main()
 250