test: add functional test for getopenrpcinfo
What changed, and why it matters
This commit only adds a new automated test that checks the output of an existing Bitcoin RPC command called getopenrpcinfo. It does not change any production code, wallet logic, networking, or consensus rules. There is no security issue here.
No security action needed. This is a routine test-only addition and can be reviewed as normal QA.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces test/functional/rpc_openrpc.py, a functional regression test for the getopenrpcinfo RPC, and registers it in test_runner.py. The test verifies JSON serializability and inspects OpenRPC document fields (method/param schemas, hidden-command behavior, type overrides, fixed-length arrays, fallback annotations, and unit annotations). No C++/Python node code is modified.
Changed components
test/functional/rpc_openrpc.pytest/functional/test_runner.pyInspect captured patch +98 / −0
diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py
new file mode 100755
index 00000000..b5d24d80
--- /dev/null
+++ b/test/functional/rpc_openrpc.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# Copyright (c) 2026 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Check that getopenrpcinfo RPC is callable and serializable as valid json."""
+
+import json
+
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal
+
+
+def find_method(openrpc, name):
+ return next(method for method in openrpc["methods"] if method["name"] == name)
+
+
+def find_param(method, name):
+ return next(param for param in method["params"] if param["name"] == name)
+
+
+class OpenRPCDocTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 1
+
+ def run_test(self):
+ self.log.info("Calling getopenrpcinfo")
+ openrpc = self.nodes[0].getopenrpcinfo()
+ json.dumps(openrpc)
+ assert_equal(openrpc["openrpc"], "1.4.1")
+ assert_equal(type(openrpc["openrpc"]).__name__, "str")
+ assert_equal(type(openrpc["info"]).__name__, "dict")
+ assert_equal(type(openrpc["methods"]).__name__, "list")
+
+ stop = find_method(openrpc, "stop")
+ assert "wait" not in [param["name"] for param in stop["params"]]
+
+ self.log.info("Calling getopenrpcinfo with hidden commands")
+ openrpc_with_hidden = self.nodes[0].getopenrpcinfo(True)
+ stop_with_hidden = find_method(openrpc_with_hidden, "stop")
+ wait = find_param(stop_with_hidden, "wait")
+ assert_equal(wait["schema"]["type"], "number")
+
+ self.log.info("Checking type_str override schemas")
+ getblockstats = find_method(openrpc, "getblockstats")
+ hash_or_height = find_param(getblockstats, "hash_or_height")
+ assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "number"}, {"type": "string"}]})
+
+ self.log.info("Checking fixed-length array schemas")
+ deriveaddresses = find_method(openrpc, "deriveaddresses")
+ range_array = find_param(deriveaddresses, "range")["schema"]["oneOf"][1]
+ assert_equal(range_array, {
+ "type": "array",
+ "items": [{"type": "number"}, {"type": "number"}],
+ "additionalItems": False,
+ "minItems": 2,
+ "maxItems": 2,
+ })
+
+ feerate_percentiles = getblockstats["result"]["schema"]["properties"]["feerate_percentiles"]
+ assert_equal(feerate_percentiles["type"], "array")
+ assert_equal(feerate_percentiles["items"], [{"type": "number"}] * 5)
+ assert_equal(feerate_percentiles["additionalItems"], False)
+ assert_equal(feerate_percentiles["minItems"], 5)
+ assert_equal(feerate_percentiles["maxItems"], 5)
+
+ self.log.info("Checking relaxed schemas for unchecked RPC types")
+ createrawtransaction = find_method(openrpc, "createrawtransaction")
+ outputs = find_param(createrawtransaction, "outputs")
+ assert_equal(outputs["schema"], {"oneOf": [{"type": "array"}, {"type": "object"}]})
+
+ getdescriptoractivity = find_method(openrpc, "getdescriptoractivity")
+ activity = getdescriptoractivity["result"]["schema"]["properties"]["activity"]
+ assert_equal(activity["type"], "array")
+
+ if "getwalletinfo" in [method["name"] for method in openrpc["methods"]]:
+ getwalletinfo = find_method(openrpc, "getwalletinfo")
+ scanning = getwalletinfo["result"]["schema"]["properties"]["scanning"]
+ assert_equal(scanning["oneOf"][0]["type"], "object")
+ assert_equal(scanning["oneOf"][1], {"const": False})
+
+ self.log.info("Checking argument fallback annotations")
+ getblock = find_method(openrpc, "getblock")
+ verbosity = find_param(getblock, "verbosity")
+ assert_equal(verbosity["schema"]["default"], 1)
+ stats = find_param(getblockstats, "stats")
+ assert_equal(stats["schema"]["x-bitcoin-default-hint"], "all values")
+
+ self.log.info("Checking string amount result annotations")
+ analyzepsbt = find_method(openrpc, "analyzepsbt")
+ result_schema = analyzepsbt["result"]["schema"]
+ estimated_feerate = result_schema["properties"]["estimated_feerate"]
+ assert_equal(estimated_feerate["type"], "string")
+ assert_equal(estimated_feerate["x-bitcoin-unit"], "amount")
+
+
+if __name__ == "__main__":
+ OpenRPCDocTest(__file__).main()
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index 5bfc7d86..646d835c 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -393,6 +393,7 @@ BASE_SCRIPTS = [
'wallet_migration.py',
'p2p_ibd_txrelay.py',
'p2p_seednode.py',
+ 'rpc_openrpc.py',
# Don't append tests at the end to avoid merge conflicts
# Put them in a random line within the section that fits their approximate run-time
]
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.