From 3cd1cd3ad3bb880950d6715cf13a4d85412e6245 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Thu, 17 Apr 2025 09:40:30 -0400 Subject: [PATCH 1/7] ipc: Add MakeBasicInit function Add a MakeBasicInit() function so simpler standalone IPC clients like bitcoin-mine in #30437 and bitcoin-cli in #32297 that only initiate IPC connections without exposing any IPC interfaces themselves can to avoid needing to implement their own specialized interfaces::Init subclasses. --- src/init/basic.cpp | 26 ++++++++++++++++++++++++++ src/interfaces/init.h | 19 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/init/basic.cpp diff --git a/src/init/basic.cpp b/src/init/basic.cpp new file mode 100644 index 00000000000..12d468c0af9 --- /dev/null +++ b/src/init/basic.cpp @@ -0,0 +1,26 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include + +namespace init { +namespace { +class BitcoinBasicInit : public interfaces::Init +{ +public: + BitcoinBasicInit(const char* exe_name, const char* process_argv0) : m_ipc(interfaces::MakeIpc(exe_name, process_argv0, *this)) {} + interfaces::Ipc* ipc() override { return m_ipc.get(); } +private: + std::unique_ptr m_ipc; +}; +} // namespace +} // namespace init + +namespace interfaces { +std::unique_ptr MakeBasicInit(const char* exe_name, const char* process_argv0) +{ + return std::make_unique(exe_name, process_argv0); +} +} // namespace interfaces diff --git a/src/interfaces/init.h b/src/interfaces/init.h index 463d43e7c2e..f214b2878fb 100644 --- a/src/interfaces/init.h +++ b/src/interfaces/init.h @@ -55,6 +55,25 @@ std::unique_ptr MakeWalletInit(int argc, char* argv[], int& exit_status); //! Return implementation of Init interface for the gui process. std::unique_ptr MakeGuiInit(int argc, char* argv[]); + +//! Return implementation of Init interface for a basic IPC client that doesn't +//! provide any IPC services itself. +//! +//! When an IPC client connects to a socket or spawns a process, it gets a pointer +//! to an Init object allowing it to create objects and threads on the remote +//! side of the IPC connection. But the client also needs to provide a local Init +//! object to allow the remote side of the connection to create objects and +//! threads on this side. This function just returns a basic Init object +//! allowing remote connections to only create local threads, not other objects +//! (because its Init::make* methods return null.) +//! +//! @param exe_name Current executable name, which is just passed to the IPC +//! system and used for logging. +//! +//! @param process_argv0 Optional string containing argv[0] value passed to +//! main(). This is passed to the IPC system and used to locate binaries by +//! relative path if subprocesses are spawned. +std::unique_ptr MakeBasicInit(const char* exe_name, const char* process_argv0=""); } // namespace interfaces #endif // BITCOIN_INTERFACES_INIT_H From df76891a3b492030eef85ede98d0559d09c464fd Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Thu, 17 Apr 2025 09:34:36 -0400 Subject: [PATCH 2/7] refactor: Add ExecuteHTTPRPC function Add ExecuteHTTPRPC to provide a way to execute an HTTP request without relying on HTTPRequest and libevent types. Behavior is not changing in any way, this is just moving code. This commit may be easiest to review using git's --color-moved option. --- src/httprpc.cpp | 144 ++++++++++++++++++++++++--------------------- src/httprpc.h | 9 +++ src/httpserver.h | 2 +- src/rpc/protocol.h | 2 +- 4 files changed, 87 insertions(+), 70 deletions(-) diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 56a243085c2..2a6751df332 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -38,13 +38,15 @@ static std::vector> g_rpcauth; static std::map> g_rpc_whitelist; static bool g_rpc_whitelist_default = false; -static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCRequest& jreq) +static UniValue JSONErrorReply(UniValue objError, const JSONRPCRequest& jreq, HTTPStatusCode& nStatus) { - // Sending HTTP errors is a legacy JSON-RPC behavior. + // HTTP errors should never be returned if JSON-RPC v2 was requested. This + // function should only be called when a v1 request fails or when a request + // cannot be parsed, so the version is unknown. Assume(jreq.m_json_version != JSONRPCVersion::V2); // Send error reply from json-rpc error object - int nStatus = HTTP_INTERNAL_SERVER_ERROR; + nStatus = HTTP_INTERNAL_SERVER_ERROR; int code = objError.find_value("code").getInt(); if (code == RPC_INVALID_REQUEST) @@ -52,10 +54,7 @@ static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCReq else if (code == RPC_METHOD_NOT_FOUND) nStatus = HTTP_NOT_FOUND; - std::string strReply = JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version).write() + "\n"; - - req->WriteHeader("Content-Type", "application/json"); - req->WriteReply(nStatus, strReply); + return JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version); } //This function checks username and password against -rpcauth @@ -101,60 +100,23 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna return CheckUserAuthorized(user, pass); } -static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req) +UniValue ExecuteHTTPRPC(const UniValue& valRequest, JSONRPCRequest& jreq, HTTPStatusCode& status) { - // JSONRPC handles only POST - if (req->GetRequestMethod() != HTTPRequest::POST) { - req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests"); - return false; - } - // Check authorization - std::pair authHeader = req->GetHeader("authorization"); - if (!authHeader.first) { - req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA); - req->WriteReply(HTTP_UNAUTHORIZED); - return false; - } - - JSONRPCRequest jreq; - jreq.context = context; - jreq.peerAddr = req->GetPeer().ToStringAddrPort(); - if (!RPCAuthorized(authHeader.second, jreq.authUser)) { - LogWarning("ThreadRPCServer incorrect password attempt from %s", jreq.peerAddr); - - /* Deter brute-forcing - If this results in a DoS the user really - shouldn't have their RPC port exposed. */ - UninterruptibleSleep(std::chrono::milliseconds{250}); - - req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA); - req->WriteReply(HTTP_UNAUTHORIZED); - return false; - } - + status = HTTP_OK; try { - // Parse request - UniValue valRequest; - if (!valRequest.read(req->ReadBody())) - throw JSONRPCError(RPC_PARSE_ERROR, "Parse error"); - - // Set the URI - jreq.URI = req->GetURI(); - - UniValue reply; bool user_has_whitelist = g_rpc_whitelist.contains(jreq.authUser); if (!user_has_whitelist && g_rpc_whitelist_default) { LogWarning("RPC User %s not allowed to call any methods", jreq.authUser); - req->WriteReply(HTTP_FORBIDDEN); - return false; + status = HTTP_FORBIDDEN; + return {}; // singleton request } else if (valRequest.isObject()) { jreq.parse(valRequest); if (user_has_whitelist && !g_rpc_whitelist[jreq.authUser].contains(jreq.strMethod)) { LogWarning("RPC User %s not allowed to call method %s", jreq.authUser, jreq.strMethod); - req->WriteReply(HTTP_FORBIDDEN); - return false; + status = HTTP_FORBIDDEN; + return {}; } // Legacy 1.0/1.1 behavior is for failed requests to throw @@ -162,14 +124,13 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req) // 2.0 behavior is to catch exceptions and return HTTP success with // RPC errors, as long as there is not an actual HTTP server error. const bool catch_errors{jreq.m_json_version == JSONRPCVersion::V2}; - reply = JSONRPCExec(jreq, catch_errors); - + UniValue reply{JSONRPCExec(jreq, catch_errors)}; if (jreq.IsNotification()) { // Even though we do execute notifications, we do not respond to them - req->WriteReply(HTTP_NO_CONTENT); - return true; + status = HTTP_NO_CONTENT; + return {}; } - + return reply; // array of requests } else if (valRequest.isArray()) { // Check authorization for each request's method @@ -183,15 +144,15 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req) std::string strMethod = request.find_value("method").get_str(); if (!g_rpc_whitelist[jreq.authUser].contains(strMethod)) { LogWarning("RPC User %s not allowed to call method %s", jreq.authUser, strMethod); - req->WriteReply(HTTP_FORBIDDEN); - return false; + status = HTTP_FORBIDDEN; + return {}; } } } } // Execute each request - reply = UniValue::VARR; + UniValue reply = UniValue::VARR; for (size_t i{0}; i < valRequest.size(); ++i) { // Batches never throw HTTP errors, they are always just included // in "HTTP OK" responses. Notifications never get any response. @@ -218,23 +179,70 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req) // empty response in this case to favor being backwards compatible // over complying with the JSON-RPC 2.0 spec in this case. if (reply.size() == 0 && valRequest.size() > 0) { - req->WriteReply(HTTP_NO_CONTENT); - return true; + status = HTTP_NO_CONTENT; + return {}; } + return reply; } else throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error"); - - req->WriteHeader("Content-Type", "application/json"); - req->WriteReply(HTTP_OK, reply.write() + "\n"); } catch (UniValue& e) { - JSONErrorReply(req, std::move(e), jreq); - return false; + return JSONErrorReply(std::move(e), jreq, status); } catch (const std::exception& e) { - JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq); - return false; + return JSONErrorReply(JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq, status); + } +} + +static void HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req) +{ + // JSONRPC handles only POST + if (req->GetRequestMethod() != HTTPRequest::POST) { + req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests"); + return; + } + // Check authorization + std::pair authHeader = req->GetHeader("authorization"); + if (!authHeader.first) { + req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA); + req->WriteReply(HTTP_UNAUTHORIZED); + return; + } + + JSONRPCRequest jreq; + jreq.context = context; + jreq.peerAddr = req->GetPeer().ToStringAddrPort(); + jreq.URI = req->GetURI(); + if (!RPCAuthorized(authHeader.second, jreq.authUser)) { + LogWarning("ThreadRPCServer incorrect password attempt from %s", jreq.peerAddr); + + /* Deter brute-forcing + If this results in a DoS the user really + shouldn't have their RPC port exposed. */ + UninterruptibleSleep(std::chrono::milliseconds{250}); + + req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA); + req->WriteReply(HTTP_UNAUTHORIZED); + return; + } + + // Generate reply + HTTPStatusCode status; + UniValue reply; + UniValue request; + if (request.read(req->ReadBody())) { + reply = ExecuteHTTPRPC(request, jreq, status); + } else { + reply = JSONErrorReply(JSONRPCError(RPC_PARSE_ERROR, "Parse error"), jreq, status); + } + + // Write reply + if (reply.isNull()) { + // Error case or no-content notification reply. + req->WriteReply(status); + } else { + req->WriteHeader("Content-Type", "application/json"); + req->WriteReply(status, reply.write() + "\n"); } - return true; } static bool InitRPCAuthentication() diff --git a/src/httprpc.h b/src/httprpc.h index 1c1a624168f..511c82a4459 100644 --- a/src/httprpc.h +++ b/src/httprpc.h @@ -7,6 +7,10 @@ #include +class JSONRPCRequest; +class UniValue; +enum HTTPStatusCode : int; + /** Start HTTP RPC subsystem. * Precondition; HTTP and RPC has been started. */ @@ -19,6 +23,11 @@ void InterruptHTTPRPC(); */ void StopHTTPRPC(); +/** Execute a single HTTP request containing one or more JSONRPC requests. + * Specified `jreq` will be modified and `status` will be returned. + */ +UniValue ExecuteHTTPRPC(const UniValue& valRequest, JSONRPCRequest& jreq, HTTPStatusCode& status); + /** Start HTTP REST subsystem. * Precondition; HTTP and RPC has been started. */ diff --git a/src/httpserver.h b/src/httpserver.h index 5461480d44f..76381a382b3 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -50,7 +50,7 @@ void StopHTTPServer(); void UpdateHTTPServerLogging(bool enable); /** Handler for requests to a certain HTTP path */ -typedef std::function HTTPRequestHandler; +typedef std::function HTTPRequestHandler; /** Register handler for prefix. * If multiple handlers match a prefix, the first-registered one will * be invoked. diff --git a/src/rpc/protocol.h b/src/rpc/protocol.h index 3f18365c50b..40e685d5868 100644 --- a/src/rpc/protocol.h +++ b/src/rpc/protocol.h @@ -7,7 +7,7 @@ #define BITCOIN_RPC_PROTOCOL_H //! HTTP status codes -enum HTTPStatusCode +enum HTTPStatusCode : int { HTTP_OK = 200, HTTP_NO_CONTENT = 204, From 6a548348956b6c09e7b0c227df7d68b11f05e8eb Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Thu, 9 Oct 2025 12:47:27 -0400 Subject: [PATCH 3/7] ipc: Expose an RPC interface over the -ipcbind socket This allows `bitcoin-cli` to connect to the node via IPC instead TCP to execute RPC methods in an upcoming commit. --- src/init.cpp | 2 +- src/init/bitcoin-gui.cpp | 2 ++ src/init/bitcoin-node.cpp | 2 ++ src/interfaces/README.md | 2 ++ src/interfaces/init.h | 2 ++ src/interfaces/rpc.h | 31 +++++++++++++++++++++++++++++++ src/ipc/CMakeLists.txt | 1 + src/ipc/capnp/init-types.h | 1 + src/ipc/capnp/init.capnp | 2 ++ src/ipc/capnp/rpc-types.h | 12 ++++++++++++ src/ipc/capnp/rpc.capnp | 17 +++++++++++++++++ src/node/interfaces.cpp | 22 ++++++++++++++++++++++ 12 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/interfaces/rpc.h create mode 100644 src/ipc/capnp/rpc-types.h create mode 100644 src/ipc/capnp/rpc.capnp diff --git a/src/init.cpp b/src/init.cpp index 6a6e7a925b2..da024bdde42 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -719,7 +719,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-rpcworkqueue=", strprintf("Set the maximum depth of the work queue to service RPC calls (default: %d)", DEFAULT_HTTP_WORKQUEUE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC); argsman.AddArg("-server", "Accept command line and JSON-RPC commands", ArgsManager::ALLOW_ANY, OptionsCategory::RPC); if (can_listen_ipc) { - argsman.AddArg("-ipcbind=
", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, /node.sock, or \"unix:/custom/path\" to specify a custom path. Can be specified multiple times to listen on multiple paths. Default behavior is not to listen on any path. If relative paths are specified, they are interpreted relative to the network data directory. If paths include any parent directory components and the parent directories do not exist, they will be created.", ArgsManager::ALLOW_ANY, OptionsCategory::IPC); + argsman.AddArg("-ipcbind=
", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, /node.sock, or \"unix:/custom/path\" to specify a custom path. Can be specified multiple times to listen on multiple paths. Default behavior is not to listen on any path. If relative paths are specified, they are interpreted relative to the network data directory. If paths include any parent directory components and the parent directories do not exist, they will be created. Enabling this gives local processes that can access the socket unauthenticated RPC access, so it's important to choose a path with secure permissions if customizing this.", ArgsManager::ALLOW_ANY, OptionsCategory::IPC); } #if HAVE_DECL_FORK diff --git a/src/init/bitcoin-gui.cpp b/src/init/bitcoin-gui.cpp index ca3077b9bb2..02e8f063620 100644 --- a/src/init/bitcoin-gui.cpp +++ b/src/init/bitcoin-gui.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ public: return MakeWalletLoader(chain, *Assert(m_node.args)); } std::unique_ptr makeEcho() override { return interfaces::MakeEcho(); } + std::unique_ptr makeRpc() override { return interfaces::MakeRpc(m_node); } interfaces::Ipc* ipc() override { return m_ipc.get(); } // bitcoin-gui accepts -ipcbind option even though it does not use it // directly. It just returns true here to accept the option because diff --git a/src/init/bitcoin-node.cpp b/src/init/bitcoin-node.cpp index e42521938a6..b4e5b8eeb6d 100644 --- a/src/init/bitcoin-node.cpp +++ b/src/init/bitcoin-node.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ public: return MakeWalletLoader(chain, *Assert(m_node.args)); } std::unique_ptr makeEcho() override { return interfaces::MakeEcho(); } + std::unique_ptr makeRpc() override { return interfaces::MakeRpc(m_node); } interfaces::Ipc* ipc() override { return m_ipc.get(); } bool canListenIpc() override { return true; } const char* exeName() override { return EXE_NAME; } diff --git a/src/interfaces/README.md b/src/interfaces/README.md index 97167d5298f..cbb2c40f319 100644 --- a/src/interfaces/README.md +++ b/src/interfaces/README.md @@ -16,4 +16,6 @@ The following interfaces are defined here: * [`Ipc`](ipc.h) — used by multiprocess code to access `Init` interface across processes. Added in [#19160](https://github.com/bitcoin/bitcoin/pull/19160). +* [`Rpc`](rpc.h) — used by `bitcoin-cli` to be able to call RPC methods over a unix socket instead of TCP. + The interfaces above define boundaries between major components of bitcoin code (node, wallet, and gui), making it possible for them to run in [different processes](../../doc/multiprocess.md), and be tested, developed, and understood independently. These interfaces are not currently designed to be stable or to be used externally. diff --git a/src/interfaces/init.h b/src/interfaces/init.h index f214b2878fb..d5b394b0e23 100644 --- a/src/interfaces/init.h +++ b/src/interfaces/init.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,7 @@ public: virtual std::unique_ptr makeMining() { return nullptr; } virtual std::unique_ptr makeWalletLoader(Chain& chain) { return nullptr; } virtual std::unique_ptr makeEcho() { return nullptr; } + virtual std::unique_ptr makeRpc() { return nullptr; } virtual Ipc* ipc() { return nullptr; } virtual bool canListenIpc() { return false; } virtual const char* exeName() { return nullptr; } diff --git a/src/interfaces/rpc.h b/src/interfaces/rpc.h new file mode 100644 index 00000000000..d8d5566ad87 --- /dev/null +++ b/src/interfaces/rpc.h @@ -0,0 +1,31 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_INTERFACES_RPC_H +#define BITCOIN_INTERFACES_RPC_H + +#include +#include + +class UniValue; + +namespace node { +struct NodeContext; +} // namespace node + +namespace interfaces { +//! Interface giving clients ability to emulate HTTP RPC calls. +class Rpc +{ +public: + virtual ~Rpc() = default; + virtual UniValue executeRpc(UniValue request, std::string url, std::string user) = 0; +}; + +//! Return implementation of Rpc interface. +std::unique_ptr MakeRpc(node::NodeContext& node); + +} // namespace interfaces + +#endif // BITCOIN_INTERFACES_RPC_H diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt index 5378ef1924b..8326423d8bc 100644 --- a/src/ipc/CMakeLists.txt +++ b/src/ipc/CMakeLists.txt @@ -14,6 +14,7 @@ target_capnp_sources(bitcoin_ipc ${CMAKE_CURRENT_SOURCE_DIR} capnp/echo.capnp capnp/init.capnp capnp/mining.capnp + capnp/rpc.capnp ) target_link_libraries(bitcoin_ipc diff --git a/src/ipc/capnp/init-types.h b/src/ipc/capnp/init-types.h index 2abd7b211e1..c6764d2a280 100644 --- a/src/ipc/capnp/init-types.h +++ b/src/ipc/capnp/init-types.h @@ -7,5 +7,6 @@ #include #include +#include #endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H diff --git a/src/ipc/capnp/init.capnp b/src/ipc/capnp/init.capnp index a20ef2fcaf3..d95bfb7426a 100644 --- a/src/ipc/capnp/init.capnp +++ b/src/ipc/capnp/init.capnp @@ -15,11 +15,13 @@ $Proxy.includeTypes("ipc/capnp/init-types.h"); using Echo = import "echo.capnp"; using Mining = import "mining.capnp"; +using Rpc = import "rpc.capnp"; interface Init $Proxy.wrap("interfaces::Init") { construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap); makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo); makeMining @3 (context :Proxy.Context) -> (result :Mining.Mining); + makeRpc @4 (context :Proxy.Context) -> (result :Rpc.Rpc); # DEPRECATED: no longer supported; server returns an error. makeMiningOld2 @2 () -> (); diff --git a/src/ipc/capnp/rpc-types.h b/src/ipc/capnp/rpc-types.h new file mode 100644 index 00000000000..4d385dee312 --- /dev/null +++ b/src/ipc/capnp/rpc-types.h @@ -0,0 +1,12 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_IPC_CAPNP_RPC_TYPES_H +#define BITCOIN_IPC_CAPNP_RPC_TYPES_H + +#include +#include +#include + +#endif // BITCOIN_IPC_CAPNP_RPC_TYPES_H diff --git a/src/ipc/capnp/rpc.capnp b/src/ipc/capnp/rpc.capnp new file mode 100644 index 00000000000..c831424a046 --- /dev/null +++ b/src/ipc/capnp/rpc.capnp @@ -0,0 +1,17 @@ +# Copyright (c) 2025 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +@0x9c3505dc45e146ac; + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("ipc::capnp::messages"); + +using Common = import "common.capnp"; +using Proxy = import "/mp/proxy.capnp"; +$Proxy.include("interfaces/rpc.h"); +$Proxy.includeTypes("ipc/capnp/rpc-types.h"); + +interface Rpc $Proxy.wrap("interfaces::Rpc") { + executeRpc @0 (context :Proxy.Context, request :Text, uri :Text, user :Text) -> (result :Text); +} diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 6c61b2105d9..f806c40ed7c 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -12,12 +12,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -81,6 +83,7 @@ using interfaces::Handler; using interfaces::MakeSignalHandler; using interfaces::Mining; using interfaces::Node; +using interfaces::Rpc; using interfaces::WalletLoader; using kernel::ChainstateRole; using node::BlockAssembler; @@ -1011,6 +1014,24 @@ public: bool m_interrupt_mining{false}; NodeContext& m_node; }; + +class RpcImpl : public Rpc +{ +public: + explicit RpcImpl(NodeContext& node) : m_node(node) {} + + UniValue executeRpc(UniValue request, std::string uri, std::string user) override + { + JSONRPCRequest req; + req.context = &m_node; + req.URI = std::move(uri); + req.authUser = std::move(user); + HTTPStatusCode status; + return ExecuteHTTPRPC(request, req, status); + } + + NodeContext& m_node; +}; } // namespace } // namespace node @@ -1030,4 +1051,5 @@ std::unique_ptr MakeMining(node::NodeContext& context, bool wait_loaded) } return std::make_unique(context); } +std::unique_ptr MakeRpc(node::NodeContext& context) { return std::make_unique(context); } } // namespace interfaces From 8d614bfa47f08e3c5e46af0b9bf9094c0fe80eed Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Thu, 17 Apr 2025 09:40:30 -0400 Subject: [PATCH 4/7] bitcoin-cli: Add -ipcconnect option This implements an idea from Pieter Wuille https://github.com/bitcoin/bitcoin/issues/28722#issuecomment-2807026958 to allow `bitcoin-cli` to connect to the node via IPC instead of TCP, if the `ENABLE_IPC` cmake option is enabled and the node has been started with `-ipcbind`. The feature can be tested with: build/bin/bitcoin-node -regtest -ipcbind=unix -debug=ipc build/bin/bitcoin-cli -regtest -ipcconnect=unix -getinfo The `-ipconnect` parameter can also be omitted, since this change also makes `bitcoin-cli` prefer IPC over HTTP by default, and falling back to HTTP if an IPC connection can't be established. --- src/CMakeLists.txt | 5 +++- src/bitcoin-cli.cpp | 71 +++++++++++++++++++++++++++++++++++--------- src/interfaces/ipc.h | 1 + src/ipc/stub.cpp | 14 +++++++++ 4 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 src/ipc/stub.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ad18115bbc5..70dd1bab52f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -56,6 +56,8 @@ add_subdirectory(crypto) add_subdirectory(util) if(ENABLE_IPC) add_subdirectory(ipc) +else() + add_library(bitcoin_ipc STATIC EXCLUDE_FROM_ALL ipc/stub.cpp) endif() add_library(bitcoin_consensus STATIC EXCLUDE_FROM_ALL @@ -347,13 +349,14 @@ target_link_libraries(bitcoin_cli # Bitcoin Core RPC client if(BUILD_CLI) - add_executable(bitcoin-cli bitcoin-cli.cpp) + add_executable(bitcoin-cli bitcoin-cli.cpp init/basic.cpp) add_windows_resources(bitcoin-cli bitcoin-cli-res.rc) add_windows_application_manifest(bitcoin-cli) target_link_libraries(bitcoin-cli core_interface bitcoin_cli bitcoin_common + bitcoin_ipc bitcoin_util libevent::core libevent::extra diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 909ed09faac..528bcd7df1f 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -108,6 +111,7 @@ static void SetupCliArgs(ArgsManager& argsman) argsman.AddArg("-stdin", "Read extra arguments from standard input, one per line until EOF/Ctrl-D (recommended for sensitive information such as passphrases). When combined with -stdinrpcpass, the first line from standard input is used for the RPC password.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-stdinrpcpass", "Read RPC password from standard input as a single line. When combined with -stdin, the first line from standard input is used for the RPC password. When combined with -stdinwalletpassphrase, -stdinrpcpass consumes the first line, and -stdinwalletpassphrase consumes the second.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-stdinwalletpassphrase", "Read wallet passphrase from standard input as a single line. When combined with -stdin, the first line from standard input is used for the wallet passphrase.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-ipcconnect=
", "Connect to bitcoin-node through IPC socket instead of TCP socket to execute requests. Valid
values are 'auto' to try to connect to default socket path at /node.sock but fall back to TCP if it is not available, 'unix' to connect to the default socket and fail if it isn't available, or 'unix:' to connect to a socket at a nonstandard path. -noipcconnect can be specified to avoid attempting to use IPC at all. Default value: auto", ArgsManager::ALLOW_ANY, OptionsCategory::IPC); } std::optional RpcWalletName(const ArgsManager& args) @@ -792,7 +796,40 @@ struct DefaultRequestHandler : BaseRequestHandler { } }; -static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector& args, const std::optional& rpcwallet = {}) +static std::optional CallIPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector& args, const std::string& endpoint, const std::string& username) +{ + auto ipcconnect{gArgs.GetArg("-ipcconnect", "auto")}; + if (ipcconnect == "0") return {}; // Do not attempt IPC if -ipcconnect is disabled. + if (gArgs.IsArgSet("-rpcconnect") && !gArgs.IsArgNegated("-rpcconnect")) { + if (ipcconnect == "auto") return {}; // Use HTTP if -ipcconnect=auto is set and -rpcconnect is enabled. + throw std::runtime_error("-rpcconnect and -ipcconnect options cannot both be enabled"); + } + + std::unique_ptr local_init{interfaces::MakeBasicInit("bitcoin-cli")}; + if (!local_init || !local_init->ipc()) { + if (ipcconnect == "auto") return {}; // Use HTTP if -ipcconnect=auto is set and there is no IPC support. + throw std::runtime_error("bitcoin-cli was not built with IPC support"); + } + + std::unique_ptr node_init; + try { + node_init = local_init->ipc()->connectAddress(ipcconnect); + if (!node_init) return {}; // Fall back to HTTP if -ipcconnect=auto connect failed. + } catch (const std::exception& e) { + // Catch connect error if -ipcconnect=unix was specified + throw CConnectionFailed{strprintf("%s\n\n" + "Probably bitcoin-node is not running or not listening on a unix socket. Can be started with:\n\n" + " bitcoin-node -chain=%s -ipcbind=unix", e.what(), gArgs.GetChainTypeString())}; + } + + std::unique_ptr rpc{node_init->makeRpc()}; + assert(rpc); + UniValue request{rh->PrepareRequest(strMethod, args)}; + UniValue reply{rpc->executeRpc(std::move(request), endpoint, username)}; + return rh->ProcessReply(reply); +} + +static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector& args, const std::string& endpoint, const std::string& username) { std::string host; // In preference order, we choose the following for the port: @@ -873,7 +910,7 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co failedToGetAuthCookie = true; } } else { - strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", ""); + strRPCUserColonPass = username + ":" + gArgs.GetArg("-rpcpassword", ""); } struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get()); @@ -889,17 +926,6 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co assert(output_buffer); evbuffer_add(output_buffer, strRequest.data(), strRequest.size()); - // check if we should use a special wallet endpoint - std::string endpoint = "/"; - if (rpcwallet) { - char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false); - if (encodedURI) { - endpoint = "/wallet/" + std::string(encodedURI); - free(encodedURI); - } else { - throw CConnectionFailed("uri-encode failed"); - } - } int r = evhttp_make_request(evcon.get(), req.release(), EVHTTP_REQ_POST, endpoint.c_str()); if (r != 0) { throw CConnectionFailed("send http request failed"); @@ -959,9 +985,26 @@ static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& str const int timeout = gArgs.GetIntArg("-rpcwaittimeout", DEFAULT_WAIT_CLIENT_TIMEOUT); const auto deadline{std::chrono::steady_clock::now() + 1s * timeout}; + // check if we should use a special wallet endpoint + std::string endpoint = "/"; + if (rpcwallet) { + char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false); + if (encodedURI) { + endpoint = "/wallet/" + std::string(encodedURI); + free(encodedURI); + } else { + throw CConnectionFailed("uri-encode failed"); + } + } + + std::string username{gArgs.GetArg("-rpcuser", "")}; do { try { - response = CallRPC(rh, strMethod, args, rpcwallet); + if (auto ipc_response{CallIPC(rh, strMethod, args, endpoint, username)}) { + response = std::move(*ipc_response); + } else { + response = CallRPC(rh, strMethod, args, endpoint, username); + } if (fWait) { const UniValue& error = response.find_value("error"); if (!error.isNull() && error["code"].getInt() == RPC_IN_WARMUP) { diff --git a/src/interfaces/ipc.h b/src/interfaces/ipc.h index f94ffa3c9d7..23b46fb6826 100644 --- a/src/interfaces/ipc.h +++ b/src/interfaces/ipc.h @@ -7,6 +7,7 @@ #include #include +#include #include namespace ipc { diff --git a/src/ipc/stub.cpp b/src/ipc/stub.cpp new file mode 100644 index 00000000000..e00ff568f82 --- /dev/null +++ b/src/ipc/stub.cpp @@ -0,0 +1,14 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +namespace interfaces { +std::unique_ptr MakeIpc(const char* exe_name, const char* process_argv0, Init& init) +{ + return {}; +} +} // namespace interfaces From 0448a19b1bb9ac615334aab0e82a8a963d81bbd8 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Fri, 26 Jul 2024 08:47:05 -0400 Subject: [PATCH 5/7] ipc: Improve -ipcconnect error checking When an invalid socket path is passed to -ipcconnect, either because the path exceeds the maximum socket length, or the path includes a directory component which is not actually a directory, treat this the same as the same as the socket refusing connections or not existing, instead of treating it like a more serious I/O error and throwing a fatal exception. This is needed to avoid CI errors after the following commit which adds a functional test and uses -datadir paths exceeding the maximum socket length when running in CI. --- src/ipc/interfaces.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ipc/interfaces.cpp b/src/ipc/interfaces.cpp index f40913d1853..1171f706487 100644 --- a/src/ipc/interfaces.cpp +++ b/src/ipc/interfaces.cpp @@ -95,10 +95,13 @@ public: fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address); } catch (const std::system_error& e) { // If connection type is auto and socket path isn't accepting connections, or doesn't exist, catch the error and return null; - if (e.code() == std::errc::connection_refused || e.code() == std::errc::no_such_file_or_directory) { + if (e.code() == std::errc::connection_refused || e.code() == std::errc::no_such_file_or_directory || e.code() == std::errc::not_a_directory) { return nullptr; } throw; + } catch (const std::invalid_argument&) { + // Catch 'Unix address path "..." exceeded maximum socket path length' error + return nullptr; } } else { fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address); From fbea576c264b715e06141e9050dc56f496892933 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Fri, 18 Apr 2025 11:37:21 -0400 Subject: [PATCH 6/7] test: add interface_ipc_cli.py testing bitcoin-cli -ipcconnect Co-authored-by: Sjors Provoost --- test/functional/interface_bitcoin_cli.py | 11 ++++ test/functional/interface_ipc_cli.py | 68 ++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 3 files changed, 80 insertions(+) create mode 100755 test/functional/interface_ipc_cli.py diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index f626ee51761..fbe753a65fa 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -6,6 +6,7 @@ from decimal import Decimal import re +import subprocess from test_framework.blocktools import COINBASE_MATURITY from test_framework.netutil import test_ipv6_local @@ -434,6 +435,16 @@ class TestBitcoinCli(BitcoinTestFramework): self.log.info("Test that only one of -addrinfo, -generate, -getinfo, -netinfo may be specified at a time") assert_raises_process_error(1, "Only one of -getinfo, -netinfo may be specified", self.nodes[0].cli('-getinfo', '-netinfo').send_cli) + if not self.is_ipc_compiled(): + # This tests behavior when ENABLE_IPC is off. When it is on, + # behavior is checked by the interface_ipc_cli.py test. + self.log.info("Test bitcoin-cli -ipcconnect triggers error if not built with IPC support") + args = [self.binary_paths.bitcoincli, "-ipcconnect=unix", "-getinfo"] + result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + assert_equal(result.stdout, "error: bitcoin-cli was not built with IPC support\n") + assert_equal(result.stderr, None) + assert_equal(result.returncode, 1) + if __name__ == '__main__': TestBitcoinCli(__file__).main() diff --git a/test/functional/interface_ipc_cli.py b/test/functional/interface_ipc_cli.py new file mode 100755 index 00000000000..92ccc879fba --- /dev/null +++ b/test/functional/interface_ipc_cli.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test IPC with bitcoin-cli""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + rpc_port +) + +import subprocess + +class TestBitcoinIpcCli(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + self.skip_if_no_ipc() + + def setup_nodes(self): + self.extra_init = [{"ipcbind": True}] + super().setup_nodes() + + def test_cli(self, args, error=None): + # Intentionally set wrong RPC password so only IPC not HTTP connections work + args = [self.binary_paths.bitcoincli, f"-datadir={self.nodes[0].datadir_path}", "-rpcpassword=wrong"] + args + ["echo", "foo"] + result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + if error is None: + assert_equal(result.stdout, '[\n "foo"\n]\n') + else: + assert_equal(result.stdout, error) + assert_equal(result.stderr, None) + assert_equal(result.returncode, 0 if error is None else 1) + + def run_test(self): + node = self.nodes[0] + if node.ipc_tmp_dir: + self.log.info("Skipping a few checks because temporary directory path is too long") + + http_auth_error = "error: Authorization failed: Incorrect rpcuser or rpcpassword\n" + http_connect_error = f"error: timeout on transient error: Could not connect to the server 127.0.0.1:{rpc_port(node.index)}\n\nMake sure the bitcoind server is running and that you are connecting to the correct RPC port.\nUse \"bitcoin-cli -help\" for more info.\n" + ipc_connect_error = "error: timeout on transient error: Connection refused\n\nProbably bitcoin-node is not running or not listening on a unix socket. Can be started with:\n\n bitcoin-node -chain=regtest -ipcbind=unix\n" + ipc_http_conflict = "error: -rpcconnect and -ipcconnect options cannot both be enabled\n" + + for started in (True, False): + auto_error = None if started else http_connect_error + http_error = http_auth_error if started else http_connect_error + ipc_error = None if started else ipc_connect_error + + if not node.ipc_tmp_dir: + self.test_cli([], auto_error) + self.test_cli(["-rpcconnect=127.0.0.1"], http_error) + self.test_cli(["-ipcconnect=auto"], auto_error) + self.test_cli(["-ipcconnect=auto", "-rpcconnect=127.0.0.1"], http_error) + self.test_cli(["-ipcconnect=unix"], ipc_error) + + self.test_cli([f"-ipcconnect=unix:{node.ipc_socket_path}"], ipc_error) + self.test_cli(["-noipcconnect"], http_error) + self.test_cli(["-ipcconnect=unix", "-rpcconnect=127.0.0.1"], ipc_http_conflict) + + self.stop_node(0) + + +if __name__ == '__main__': + TestBitcoinIpcCli(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index a4052f91024..8d80b5a78ff 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -379,6 +379,7 @@ BASE_SCRIPTS = [ 'tool_rpcauth.py', 'p2p_handshake.py', 'p2p_handshake.py --v2transport', + 'interface_ipc_cli.py', 'feature_dirsymlinks.py', 'feature_help.py', 'feature_framework_startup_failures.py', From 4565cff72c60f56d8ad4b46716fe8df847554824 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Tue, 24 Jun 2025 17:12:11 -0400 Subject: [PATCH 7/7] bitcoin-gui: Implement missing Init::makeMining method A missing Init::makeMining implementation was causing internal code using the mining interface (like the `waitforblockheight` RPC method) to not work when running inside the `bitcoin-gui` binary. It was working the other bitcoin binaries ('bitocind`, `bitcoin-qt`, and `bitcoin-node`) because they implmemented `Init::makeMining` methods in commit 8ecb6816781c7c7f423b501cbb2de3abd7250119 from #30200, but the `bitcoin-gui` init class was forgotten in that change. This bug was reported by Matthew Zipkin https://github.com/bitcoin/bitcoin/pull/32297#pullrequestreview-2932651216 --- src/init/bitcoin-gui.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/init/bitcoin-gui.cpp b/src/init/bitcoin-gui.cpp index 02e8f063620..45f623d026a 100644 --- a/src/init/bitcoin-gui.cpp +++ b/src/init/bitcoin-gui.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ public: } std::unique_ptr makeNode() override { return interfaces::MakeNode(m_node); } std::unique_ptr makeChain() override { return interfaces::MakeChain(m_node); } + std::unique_ptr makeMining() override { return interfaces::MakeMining(m_node); } std::unique_ptr makeWalletLoader(interfaces::Chain& chain) override { return MakeWalletLoader(chain, *Assert(m_node.args));