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