cli, rpc: add -rpcid option for custom request IDs
What changed, and why it matters
This commit adds a new command-line option `-rpcid` to bitcoin-cli that lets users set their own identifier on JSON-RPC requests, and includes that identifier in server debug logs. It is a usability/debugging feature with no security relevance visible in the code or commit message.
No security action required. This is a benign feature addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces a -rpcid=<id> argument in bitcoin-cli, defaulting to the previous hardcoded value “1”, and passes that string as the JSON-RPC request ID. On the server side, JSONRPCRequest::parse now logs the request ID (sanitized via SanitizeString) in debug log lines. The functional tests verify the custom ID appears in logs and that unsafe characters are sanitized. There is no change to authentication, authorization, parsing of RPC methods/params, or network behavior.
Changed components
src/bitcoin-cli.cppsrc/rpc/request.cpptest/functional/interface_bitcoin_cli.pyInspect captured patch +21 / −4
diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp
index cc195226..3455aea7 100644
--- a/src/bitcoin-cli.cpp
+++ b/src/bitcoin-cli.cpp
@@ -57,6 +57,7 @@ using CliClock = std::chrono::system_clock;
const TranslateFn G_TRANSLATION_FUN{nullptr};
static const char DEFAULT_RPCCONNECT[] = "127.0.0.1";
+static constexpr const char* DEFAULT_RPC_REQ_ID{"1"};
static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900;
static constexpr int DEFAULT_WAIT_CLIENT_TIMEOUT = 0;
static const bool DEFAULT_NAMED=false;
@@ -100,6 +101,7 @@ static void SetupCliArgs(ArgsManager& argsman)
SetupChainParamsBaseOptions(argsman);
argsman.AddArg("-color=<when>", strprintf("Color setting for CLI output (default: %s). Valid values: always, auto (add color codes when standard output is connected to a terminal and OS is not WIN32), never. Only applies to the output of -getinfo.", DEFAULT_COLOR_SETTING), ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::OPTIONS);
argsman.AddArg("-named", strprintf("Pass named instead of positional arguments (default: %s)", DEFAULT_NAMED), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
+ argsman.AddArg("-rpcid=<id>", strprintf("Set a custom JSON-RPC request ID string (default: %s)", DEFAULT_RPC_REQ_ID), ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION | ArgsManager::DISALLOW_ELISION, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcclienttimeout=<n>", strprintf("Timeout in seconds during HTTP requests, or 0 for no timeout. (default: %d)", DEFAULT_HTTP_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcconnect=<ip>", strprintf("Send commands to node running on <ip> (default: %s)", DEFAULT_RPCCONNECT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpccookiefile=<loc>", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@@ -787,7 +789,8 @@ struct DefaultRequestHandler : BaseRequestHandler {
} else {
params = RPCConvertValues(method, args);
}
- return JSONRPCRequestObj(method, params, 1);
+ UniValue id{UniValue::VSTR, gArgs.GetArg("-rpcid", DEFAULT_RPC_REQ_ID)};
+ return JSONRPCRequestObj(method, params, id);
}
UniValue ProcessReply(const UniValue &reply) override
diff --git a/src/rpc/request.cpp b/src/rpc/request.cpp
index 34a9ff75..d162263e 100644
--- a/src/rpc/request.cpp
+++ b/src/rpc/request.cpp
@@ -233,11 +233,13 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
if (!valMethod.isStr())
throw JSONRPCError(RPC_INVALID_REQUEST, "Method must be a string");
strMethod = valMethod.get_str();
+ const std::string log_id{id && !id->isNull() ? SanitizeString(id->getValStr()) : ""};
if (fLogIPs)
- LogDebug(BCLog::RPC, "ThreadRPCServer method=%s user=%s peeraddr=%s\n", SanitizeString(strMethod),
- this->authUser, this->peerAddr);
+ LogDebug(BCLog::RPC, "ThreadRPCServer method=%s user=%s peeraddr=%s id=%s", SanitizeString(strMethod),
+ this->authUser, this->peerAddr, log_id);
else
- LogDebug(BCLog::RPC, "ThreadRPCServer method=%s user=%s\n", SanitizeString(strMethod), this->authUser);
+ LogDebug(BCLog::RPC, "ThreadRPCServer method=%s user=%s id=%s", SanitizeString(strMethod), this->authUser,
+ log_id);
// Parse params
const UniValue& valParams{request.find_value("params")};
diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py
index 953eea3d..e908ba8a 100755
--- a/test/functional/interface_bitcoin_cli.py
+++ b/test/functional/interface_bitcoin_cli.py
@@ -421,6 +421,18 @@ class TestBitcoinCli(BitcoinTestFramework):
self.test_netinfo()
+ self.log.info("Test -rpcid option sets custom JSON-RPC request ID")
+ with self.nodes[0].assert_debug_log(expected_msgs=['id=myrpcid']):
+ self.nodes[0].cli('-rpcid=myrpcid').getblockcount()
+
+ self.log.info("Test default request logs default id=1")
+ with self.nodes[0].assert_debug_log(expected_msgs=["ThreadRPCServer method=getblockcount", "id=1"]):
+ self.nodes[0].cli.getblockcount()
+
+ self.log.info("Test that request ids with unsafe characters are sanitized in the log")
+ with self.nodes[0].assert_debug_log(expected_msgs=["ThreadRPCServer method=getblockcount", "id=abcdef"]):
+ self.nodes[0].cli('-rpcid=abc<\n>def').getblockcount()
+
self.log.info("Test -version with node stopped")
self.stop_node(0)
cli_response = self.nodes[0].cli('-version').send_cli()
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.