What changed, and why it matters
This commit adds a new read-only alias 'rpc.discover' that returns the same public API documentation already available via 'getopenrpcinfo'. It also fixes a metadata annotation so amount fields are described as numbers rather than strings. There is no security-relevant change: no new privileges, no authentication bypass, no code execution, and no bug fix for a vulnerability.
No security action required. Treat as a routine feature/test update during normal review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch registers an additional RPC command ‘rpc.discover’ that calls tableRPC.buildOpenRPCDoc(false), identical to getopenrpcinfo’s default behavior. It refactors the result schema into a shared helper OpenRPCDocResult() and changes the OpenRPC type annotation for RPCResult::Type::STR_AMOUNT from ‘string’ to ‘number’. Fuzzing and functional tests are updated accordingly. No input parsing, authentication, consensus, or wallet logic is modified.
Changed components
src/rpc/server.cppsrc/test/fuzz/rpc.cpptest/functional/rpc_openrpc.pyInspect captured patch +73 / −39
diff --git a/src/rpc/server.cpp b/src/rpc/server.cpp
index ead9733d..0ecba025 100644
--- a/src/rpc/server.cpp
+++ b/src/rpc/server.cpp
@@ -442,7 +442,7 @@ UniValue OpenRPCResultSchema(const RPCResult& result)
case RPCResult::Type::STR:
return MakeObject({{"type", "string"}});
case RPCResult::Type::STR_AMOUNT:
- return MakeObject({{"type", "string"}, {"x-bitcoin-unit", "amount"}});
+ return MakeObject({{"type", "number"}, {"x-bitcoin-unit", "amount"}});
case RPCResult::Type::STR_HEX:
return MakeObject({{"type", "string"}, {"pattern", "^[0-9a-fA-F]+$"}});
case RPCResult::Type::NUM:
@@ -511,6 +511,46 @@ UniValue OpenRPCResultSchema(const RPCResult& result)
}
} // namespace
+static RPCResult OpenRPCDocResult()
+{
+ return RPCResult{
+ RPCResult::Type::OBJ, "", "",
+ {
+ {RPCResult::Type::STR, "openrpc", "OpenRPC specification version."},
+ {RPCResult::Type::OBJ, "info", "Metadata about this JSON-RPC interface.",
+ {
+ {RPCResult::Type::STR, "title", "API title."},
+ {RPCResult::Type::STR, "version", "Bitcoin Core version string."},
+ {RPCResult::Type::STR, "description", "API description."},
+ }},
+ {RPCResult::Type::ARR, "methods", "Documented RPC methods.",
+ {{RPCResult::Type::OBJ, "", "An RPC method description object.",
+ {
+ {RPCResult::Type::STR, "name", "Method name."},
+ {RPCResult::Type::STR, "description", "Method description."},
+ {RPCResult::Type::ARR, "params", "Method parameters.",
+ {{RPCResult::Type::OBJ, "", "A parameter.",
+ {
+ {RPCResult::Type::STR, "name", "Parameter name."},
+ {RPCResult::Type::BOOL, "required", "Whether the parameter is required."},
+ {RPCResult::Type::ANY, "schema", "JSON Schema for the parameter."},
+ {RPCResult::Type::STR, "description", /*optional=*/true, "Parameter description."},
+ {RPCResult::Type::ARR, "x-bitcoin-aliases", /*optional=*/true, "Alternative parameter names.",
+ {{RPCResult::Type::STR, "", "An alias."}}},
+ {RPCResult::Type::BOOL, "x-bitcoin-placeholder", /*optional=*/true, "Whether the parameter is retained only for compatibility."},
+ {RPCResult::Type::BOOL, "x-bitcoin-also-positional", /*optional=*/true, "Whether the parameter can also be passed positionally."},
+ }}}},
+ {RPCResult::Type::OBJ, "result", "Method result.",
+ {
+ {RPCResult::Type::STR, "name", "Result name."},
+ {RPCResult::Type::ANY, "schema", "JSON Schema for the result."},
+ }},
+ {RPCResult::Type::STR, "x-bitcoin-category", "RPC category."},
+ }}}},
+ },
+ {.skip_type_check = true}};
+}
+
static RPCMethod getopenrpcinfo()
{
return RPCMethod{
@@ -519,42 +559,7 @@ static RPCMethod getopenrpcinfo()
{
{"show_hidden", RPCArg::Type::BOOL, RPCArg::Default{false}, "Also include hidden RPC commands and arguments."},
},
- RPCResult{
- RPCResult::Type::OBJ, "", "",
- {
- {RPCResult::Type::STR, "openrpc", "OpenRPC specification version."},
- {RPCResult::Type::OBJ, "info", "Metadata about this JSON-RPC interface.",
- {
- {RPCResult::Type::STR, "title", "API title."},
- {RPCResult::Type::STR, "version", "Bitcoin Core version string."},
- {RPCResult::Type::STR, "description", "API description."},
- }},
- {RPCResult::Type::ARR, "methods", "Documented RPC methods.",
- {{RPCResult::Type::OBJ, "", "An RPC method description object.",
- {
- {RPCResult::Type::STR, "name", "Method name."},
- {RPCResult::Type::STR, "description", "Method description."},
- {RPCResult::Type::ARR, "params", "Method parameters.",
- {{RPCResult::Type::OBJ, "", "A parameter.",
- {
- {RPCResult::Type::STR, "name", "Parameter name."},
- {RPCResult::Type::BOOL, "required", "Whether the parameter is required."},
- {RPCResult::Type::ANY, "schema", "JSON Schema for the parameter."},
- {RPCResult::Type::STR, "description", /*optional=*/true, "Parameter description."},
- {RPCResult::Type::ARR, "x-bitcoin-aliases", /*optional=*/true, "Alternative parameter names.",
- {{RPCResult::Type::STR, "", "An alias."}}},
- {RPCResult::Type::BOOL, "x-bitcoin-placeholder", /*optional=*/true, "Whether the parameter is retained only for compatibility."},
- {RPCResult::Type::BOOL, "x-bitcoin-also-positional", /*optional=*/true, "Whether the parameter can also be passed positionally."},
- }}}},
- {RPCResult::Type::OBJ, "result", "Method result.",
- {
- {RPCResult::Type::STR, "name", "Result name."},
- {RPCResult::Type::ANY, "schema", "JSON Schema for the result."},
- }},
- {RPCResult::Type::STR, "x-bitcoin-category", "RPC category."},
- }}}},
- },
- {.skip_type_check = true}},
+ OpenRPCDocResult(),
RPCExamples{
HelpExampleCli("getopenrpcinfo", "")
+ HelpExampleRpc("getopenrpcinfo", "")
@@ -567,9 +572,28 @@ static RPCMethod getopenrpcinfo()
};
}
+static RPCMethod rpc_discover()
+{
+ return RPCMethod{
+ "rpc.discover",
+ "Returns an OpenRPC schema as a description of this service.\n",
+ {},
+ OpenRPCDocResult(),
+ RPCExamples{
+ HelpExampleCli("rpc.discover", "")
+ + HelpExampleRpc("rpc.discover", "")
+ },
+ [](const RPCMethod&, const JSONRPCRequest&) -> UniValue
+{
+ return tableRPC.buildOpenRPCDoc(/*include_hidden=*/false);
+},
+ };
+}
+
static const CRPCCommand vRPCCommands[]{
/* Overall control/query calls */
{"control", &getopenrpcinfo},
+ {"control", &rpc_discover},
{"control", &getrpcinfo},
{"control", &help},
{"control", &stop},
diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp
index 74fbb1c5..6030e29b 100644
--- a/src/test/fuzz/rpc.cpp
+++ b/src/test/fuzz/rpc.cpp
@@ -170,6 +170,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"prioritisetransaction",
"pruneblockchain",
"reconsiderblock",
+ "rpc.discover",
"scanblocks",
"scantxoutset",
"sendmsgtopeer", // when no peers are connected, no p2p message is sent
diff --git a/test/functional/rpc_openrpc.py b/test/functional/rpc_openrpc.py
index b5d24d80..91195b6d 100755
--- a/test/functional/rpc_openrpc.py
+++ b/test/functional/rpc_openrpc.py
@@ -31,6 +31,15 @@ class OpenRPCDocTest(BitcoinTestFramework):
assert_equal(type(openrpc["info"]).__name__, "dict")
assert_equal(type(openrpc["methods"]).__name__, "list")
+ self.log.info("Calling rpc.discover")
+ if self.options.usecli:
+ discovered = self.nodes[0].cli("rpc.discover").send_cli()
+ else:
+ discovered = self.nodes[0].rpc.discover()
+ assert_equal(discovered, openrpc)
+ rpc_discover = find_method(discovered, "rpc.discover")
+ assert_equal(rpc_discover["params"], [])
+
stop = find_method(openrpc, "stop")
assert "wait" not in [param["name"] for param in stop["params"]]
@@ -85,11 +94,11 @@ class OpenRPCDocTest(BitcoinTestFramework):
stats = find_param(getblockstats, "stats")
assert_equal(stats["schema"]["x-bitcoin-default-hint"], "all values")
- self.log.info("Checking string amount result annotations")
+ self.log.info("Checking numeric 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["type"], "number")
assert_equal(estimated_feerate["x-bitcoin-unit"], "amount")
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.