Merge bitcoin/bitcoin#35946: rpc: Improve some type specs for openrpc
What changed, and why it matters
This commit improves the automatically generated JSON schema that describes Bitcoin Core's RPC (remote procedure call) interface. It fixes a crash in a third-party OpenRPC code generator by producing more complete type descriptions, and tightens one type label from 'number' to 'integer' to match what the RPC actually accepts. There is no runtime code change that processes user transactions, blocks, or network data, so it does not introduce or fix a security vulnerability in the Bitcoin node itself.
No security action required; treat as a normal code-quality/documentation improvement.
Security signals we found
No security-relevant code path modified
Schema-only / documentation-only change
No input parsing, validation, or authorization logic changed
No bug fix for memory safety, cryptography, or consensus
Evidence from the diff
The change is confined to src/rpc/server.cpp and the rpc_openrpc.py functional test. It adds a propagation flag for skip-type-check arguments, surfaces descriptions for OBJ_USER_KEYS schemas, switches oneOf to anyOf when inside a skip-type-check array, and changes ApplyTypeStrOverride’s numeric branch from ‘number’ to ‘integer’. These are schema/documentation-generation corrections only; no consensus, networking, wallet, or RPC execution logic is modified.
Changed components
src/rpc/server.cpp OpenRPC schema generatortest/functional/rpc_openrpc.pyInspect captured patch +25 / −14
### src/rpc/server.cpp
@@ -243,7 +243,7 @@ static RPCMethod getrpcinfo()
}
namespace {
-UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden);
+UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden, bool in_skip_type_check);
UniValue OpenRPCResultSchema(const RPCResult& result);
UniValue MakeObject(std::initializer_list<std::pair<std::string, UniValue>> entries)
@@ -262,21 +262,21 @@ void PushUniqueSchema(UniValue& schemas, std::unordered_set<std::string>& seen,
}
// NOLINTNEXTLINE(misc-no-recursion)
-UniValue DedupArrayItemsSchema(std::span<const RPCArg> inner, bool include_hidden)
+UniValue DedupArrayItemsSchema(std::span<const RPCArg> inner, bool include_hidden, bool in_skip_type_check)
{
if (inner.empty()) return UniValue{UniValue::VOBJ};
- if (inner.size() == 1) return OpenRPCArgSchema(inner.front(), include_hidden);
+ if (inner.size() == 1) return OpenRPCArgSchema(inner.front(), include_hidden, in_skip_type_check);
UniValue one_of{UniValue::VARR};
std::unordered_set<std::string> seen;
for (const auto& item : inner) {
- PushUniqueSchema(one_of, seen, OpenRPCArgSchema(item, include_hidden));
+ PushUniqueSchema(one_of, seen, OpenRPCArgSchema(item, include_hidden, in_skip_type_check));
}
if (one_of.size() == 1) return one_of[0];
UniValue items{UniValue::VOBJ};
- items.pushKV("oneOf", std::move(one_of));
+ items.pushKV(in_skip_type_check ? "anyOf" : "oneOf", std::move(one_of));
return items;
}
@@ -311,7 +311,7 @@ void ApplyTypeStrOverride(UniValue& schema, const RPCArg& arg)
};
if (number_or_string.contains(type_label)) {
UniValue one_of{UniValue::VARR};
- one_of.push_back(MakeObject({{"type", "number"}}));
+ one_of.push_back(MakeObject({{"type", "integer"}}));
one_of.push_back(MakeObject({{"type", "string"}}));
schema = UniValue{UniValue::VOBJ};
schema.pushKV("oneOf", std::move(one_of));
@@ -331,14 +331,18 @@ void ApplyArgFallback(UniValue& schema, const RPCArg& arg)
}
// NOLINTNEXTLINE(misc-no-recursion)
-UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
+UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden, bool in_skip_type_check)
{
UniValue schema{UniValue::VOBJ};
if (arg.m_opts.skip_type_check) {
ApplyTypeStrOverride(schema, arg);
if (schema.empty() && arg.m_type == RPCArg::Type::ARR) {
+ UniValue items{UniValue::VOBJ};
+ items.pushKV("type", "array");
+ items.pushKV("items", DedupArrayItemsSchema(arg.m_inner, include_hidden, /*in_skip_type_check=*/true));
+
UniValue one_of{UniValue::VARR};
- one_of.push_back(MakeObject({{"type", "array"}}));
+ one_of.push_back(std::move(items));
one_of.push_back(MakeObject({{"type", "object"}}));
schema.pushKV("oneOf", std::move(one_of));
}
@@ -383,7 +387,7 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
break;
}
case RPCArg::Type::ARR: {
- UniValue items{DedupArrayItemsSchema(arg.m_inner, include_hidden)};
+ UniValue items{DedupArrayItemsSchema(arg.m_inner, include_hidden, in_skip_type_check)};
schema.pushKV("type", "array");
schema.pushKV("items", std::move(items));
break;
@@ -394,7 +398,7 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
UniValue required{UniValue::VARR};
for (const auto& inner : arg.m_inner) {
if (!include_hidden && inner.m_opts.hidden) continue;
- UniValue prop{OpenRPCArgSchema(inner, include_hidden)};
+ UniValue prop{OpenRPCArgSchema(inner, include_hidden, in_skip_type_check)};
if (!inner.m_description.empty()) prop.pushKV("description", inner.m_description);
if (inner.m_opts.placeholder) prop.pushKV("x-bitcoin-placeholder", true);
if (inner.m_opts.also_positional) prop.pushKV("x-bitcoin-also-positional", true);
@@ -410,7 +414,10 @@ UniValue OpenRPCArgSchema(const RPCArg& arg, bool include_hidden)
case RPCArg::Type::OBJ_USER_KEYS: {
schema.pushKV("type", "object");
if (!arg.m_inner.empty()) {
- schema.pushKV("additionalProperties", OpenRPCArgSchema(arg.m_inner[0], include_hidden));
+ schema.pushKV("additionalProperties", OpenRPCArgSchema(arg.m_inner[0], include_hidden, in_skip_type_check));
+ if (!arg.m_inner[0].m_description.empty()) {
+ schema.pushKV("description", arg.m_inner[0].m_description);
+ }
} else {
schema.pushKV("additionalProperties", true);
}
@@ -911,7 +918,7 @@ UniValue CRPCTable::buildOpenRPCDoc(bool include_hidden) const
UniValue param{UniValue::VOBJ};
param.pushKV("name", arg.GetFirstName());
param.pushKV("required", !arg.IsOptional());
- param.pushKV("schema", OpenRPCArgSchema(arg, include_hidden));
+ param.pushKV("schema", OpenRPCArgSchema(arg, include_hidden, /*in_skip_type_check=*/false));
std::vector<std::string> names{SplitString(arg.m_names, '|')};
if (names.size() > 1) {
### test/functional/rpc_openrpc.py
@@ -52,7 +52,7 @@ def run_test(self):
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"}]})
+ assert_equal(hash_or_height["schema"], {"oneOf": [{"type": "integer"}, {"type": "string"}]})
self.log.info("Checking fixed-length array schemas")
deriveaddresses = find_method(openrpc, "deriveaddresses")
@@ -75,7 +75,11 @@ def run_test(self):
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"}]})
+ address_description = "A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in BTC"
+ address_obj = {"type": "object", "additionalProperties": {"oneOf": [{"type": "number"},{"type": "string"}]}, "description": address_description}
+ data_description = "A key-value pair. The key must be \"data\", the value is hex-encoded data that becomes a part of an OP_RETURN output"
+ data_obj = {"type": "object", "properties": { "data": {"type": "string", "pattern": "^[0-9a-fA-F]+$", "description": data_description}}, "additionalProperties": False, "required": ["data"]}
+ assert_equal(outputs["schema"], {"oneOf": [{"type": "array", "items": {"anyOf": [address_obj, data_obj]}}, {"type": "object"}]})
getdescriptoractivity = find_method(openrpc, "getdescriptoractivity")
activity = getdescriptoractivity["result"]["schema"]["properties"]["activity"]Why this scored 18/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.