interface_rpc.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2018-2022 The Limenka 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 LimenkaTestFramework
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(LimenkaTestFramework):
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 nonstandard jsonrpc 1.0 version number is accepted as a Number...")
176 self.test_batch_request(lambda idx: BatchOptions(request_fields={"jsonrpc": 1.0}))
177
178 self.log.info("Testing unrecognized jsonrpc version number is accepted as if 1.0...")
179 self.test_batch_request(lambda idx: BatchOptions(
180 request_fields={"jsonrpc": "2.1"},
181 ))
182
183 def test_http_status_codes(self):
184 self.log.info("Testing HTTP status codes for JSON-RPC 1.1 requests...")
185 # OK
186 expect_http_rpc_status(200, None, self.nodes[0], "getblockhash", [0])
187 # Errors
188 expect_http_rpc_status(404, RPC_METHOD_NOT_FOUND, self.nodes[0], "invalidmethod", [])
189 expect_http_rpc_status(500, RPC_INVALID_PARAMETER, self.nodes[0], "getblockhash", [42])
190 # force-send empty request
191 response, status = send_raw_rpc(self.nodes[0], b"")
192 assert_equal(response, {"id": None, "result": None, "error": {"code": RPC_PARSE_ERROR, "message": "Parse error"}})
193 assert_equal(status, 500)
194 # force-send invalidly formatted request
195 response, status = send_raw_rpc(self.nodes[0], b"this is bad")
196 assert_equal(response, {"id": None, "result": None, "error": {"code": RPC_PARSE_ERROR, "message": "Parse error"}})
197 assert_equal(status, 500)
198
199 self.log.info("Testing HTTP status codes for JSON-RPC 2.0 requests...")
200 # OK
201 expect_http_rpc_status(200, None, self.nodes[0], "getblockhash", [0], 2, False)
202 # RPC errors but not HTTP errors
203 expect_http_rpc_status(200, RPC_METHOD_NOT_FOUND, self.nodes[0], "invalidmethod", [], 2, False)
204 expect_http_rpc_status(200, RPC_INVALID_PARAMETER, self.nodes[0], "getblockhash", [42], 2, False)
205 # force-send invalidly formatted requests
206 response, status = send_json_rpc(self.nodes[0], {"jsonrpc": 2, "method": "getblockcount"})
207 assert_equal(response, {"result": 0, "error": None})
208 assert_equal(status, 200)
209 response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "3.0", "method": "getblockcount"})
210 assert_equal(response, {"result": 0, "error": None})
211 assert_equal(status, 200)
212
213 self.log.info("Testing HTTP status codes for JSON-RPC 2.0 notifications...")
214 # Not notification: id exists
215 response, status = send_json_rpc(self.nodes[0], {"jsonrpc": "2.0", "id": None, "method": "getblockcount"})
216 assert_equal(response["result"], 0)
217 assert_equal(status, 200)
218 # Not notification: JSON 1.1
219 expect_http_rpc_status(200, None, self.nodes[0], "getblockcount", [], 1)
220 # Not notification: has "id" field
221 expect_http_rpc_status(200, None, self.nodes[0], "getblockcount", [], 2, False)
222 block_count = self.nodes[0].getblockcount()
223 # Notification response status code: HTTP_NO_CONTENT
224 expect_http_rpc_status(204, None, self.nodes[0], "generatetoaddress", [1, "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202"], 2, True)
225 # The command worked even though there was no response
226 assert_equal(block_count + 1, self.nodes[0].getblockcount())
227 # No error response for notifications even if they are invalid
228 expect_http_rpc_status(204, None, self.nodes[0], "generatetoaddress", [1, "invalid_address"], 2, True)
229 # Sanity check: command was not executed
230 assert_equal(block_count + 1, self.nodes[0].getblockcount())
231
232 def test_work_queue_exceeded(self):
233 self.log.info("Testing work queue exceeded...")
234 self.restart_node(0, ['-rpcworkqueue=1', '-rpcthreads=1'])
235 got_exceeded_error = []
236 threads = []
237 for _ in range(3):
238 t = Thread(target=test_work_queue_getblock, args=(self.nodes[0], got_exceeded_error))
239 t.start()
240 threads.append(t)
241 for t in threads:
242 t.join()
243
244 def run_test(self):
245 self.test_getrpcinfo()
246 self.test_batch_requests()
247 self.test_http_status_codes()
248 self.test_work_queue_exceeded()
249
250
251 if __name__ == '__main__':
252 RPCInterfaceTest(__file__).main()
253