This commit is contained in:
Ryan Ofsky 2025-04-29 11:51:45 +02:00 committed by GitHub
commit bf51eef4ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 407 additions and 86 deletions

View file

@ -391,6 +391,12 @@ target_link_libraries(bitcoin_cli
# Bitcoin Core RPC client
if(BUILD_CLI)
add_executable(bitcoin-cli bitcoin-cli.cpp)
if(ENABLE_IPC)
target_sources(bitcoin-cli PRIVATE init/basic-ipc.cpp)
target_link_libraries(bitcoin-cli bitcoin_ipc)
else()
target_sources(bitcoin-cli PRIVATE init/basic.cpp)
endif()
add_windows_resources(bitcoin-cli bitcoin-cli-res.rc)
target_link_libraries(bitcoin-cli
core_interface

View file

@ -11,6 +11,9 @@
#include <common/system.h>
#include <compat/compat.h>
#include <compat/stdin.h>
#include <interfaces/init.h>
#include <interfaces/ipc.h>
#include <interfaces/rpc.h>
#include <policy/feerate.h>
#include <rpc/client.h>
#include <rpc/mining.h>
@ -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=<address>", "Connect to bitcoin-node through IPC socket instead of TCP socket to execute requests. Valid <address> values are 'auto' to try to connect to default socket path at <datadir>/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:<socket path>' 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<std::string> RpcWalletName(const ArgsManager& args)
@ -775,7 +779,40 @@ struct DefaultRequestHandler : BaseRequestHandler {
}
};
static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const std::optional<std::string>& rpcwallet = {})
static std::optional<UniValue> CallIPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& 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<interfaces::Init> 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<interfaces::Init> 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 std::runtime_error{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<interfaces::Rpc> 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<std::string>& args, const std::string& endpoint, const std::string& username)
{
std::string host;
// In preference order, we choose the following for the port:
@ -856,7 +893,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());
@ -872,17 +909,7 @@ 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.get(), EVHTTP_REQ_POST, endpoint.c_str());
req.release(); // ownership moved to evcon in above call
if (r != 0) {
@ -943,9 +970,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", "")};
if (auto response{CallIPC(rh, strMethod, args, endpoint, username)}) {
return *response;
}
do {
try {
response = CallRPC(rh, strMethod, args, rpcwallet);
response = CallRPC(rh, strMethod, args, endpoint, username);
if (fWait) {
const UniValue& error = response.find_value("error");
if (!error.isNull() && error["code"].getInt<int>() == RPC_IN_WARMUP) {

View file

@ -79,13 +79,15 @@ static std::vector<std::vector<std::string>> g_rpcauth;
static std::map<std::string, std::set<std::string>> 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<int>();
if (code == RPC_INVALID_REQUEST)
@ -93,10 +95,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
@ -153,60 +152,23 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna
return multiUserAuthorized(strUserPass);
}
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<bool, std::string> 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)) {
LogPrintf("ThreadRPCServer incorrect password attempt from %s\n", 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.count(jreq.authUser);
if (!user_has_whitelist && g_rpc_whitelist_default) {
LogPrintf("RPC User %s not allowed to call any methods\n", 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].count(jreq.strMethod)) {
LogPrintf("RPC User %s not allowed to call method %s\n", 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
@ -214,14 +176,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
@ -235,15 +196,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].count(strMethod)) {
LogPrintf("RPC User %s not allowed to call method %s\n", 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.
@ -270,23 +231,71 @@ 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 JSONErrorReply(JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq, status);
}
}
static bool 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 false;
}
return true;
// Check authorization
std::pair<bool, std::string> 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();
jreq.URI = req->GetURI();
if (!RPCAuthorized(authHeader.second, jreq.authUser)) {
LogPrintf("ThreadRPCServer incorrect password attempt from %s\n", 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;
}
// 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 status < 400;
}
static bool InitRPCAuthentication()

View file

@ -7,6 +7,10 @@
#include <any>
class JSONRPCRequest;
class UniValue;
enum HTTPStatusCode : int;
/** Start HTTP RPC subsystem.
* Precondition; HTTP and RPC has been started.
*/
@ -19,6 +23,9 @@ void InterruptHTTPRPC();
*/
void StopHTTPRPC();
/** Execute a single HTTP request, containing one or more JSONRPC requests. */
UniValue ExecuteHTTPRPC(const UniValue& valRequest, JSONRPCRequest& jreq, HTTPStatusCode& status);
/** Start HTTP REST subsystem.
* Precondition; HTTP and RPC has been started.
*/

25
src/init/basic-ipc.cpp Normal file
View file

@ -0,0 +1,25 @@
// 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 <interfaces/init.h>
#include <interfaces/ipc.h>
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(); }
std::unique_ptr<interfaces::Ipc> m_ipc;
};
} // namespace
} // namespace init
namespace interfaces {
std::unique_ptr<Init> MakeBasicInit(const char* exe_name, const char* process_argv0)
{
return std::make_unique<init::BitcoinBasicInit>(exe_name, process_argv0);
}
} // namespace interfaces

12
src/init/basic.cpp Normal file
View file

@ -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.
#include <interfaces/init.h>
namespace interfaces {
std::unique_ptr<Init> MakeBasicInit(const char* exe_name, const char* process_argv0)
{
return std::make_unique<Init>();
}
} // namespace interfaces

View file

@ -8,6 +8,7 @@
#include <interfaces/init.h>
#include <interfaces/ipc.h>
#include <interfaces/node.h>
#include <interfaces/rpc.h>
#include <interfaces/wallet.h>
#include <node/context.h>
#include <util/check.h>
@ -33,6 +34,7 @@ public:
return MakeWalletLoader(chain, *Assert(m_node.args));
}
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
std::unique_ptr<interfaces::Rpc> 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

View file

@ -8,6 +8,7 @@
#include <interfaces/init.h>
#include <interfaces/ipc.h>
#include <interfaces/node.h>
#include <interfaces/rpc.h>
#include <interfaces/wallet.h>
#include <node/context.h>
#include <util/check.h>
@ -36,6 +37,7 @@ public:
return MakeWalletLoader(chain, *Assert(m_node.args));
}
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
std::unique_ptr<interfaces::Rpc> makeRpc() override { return interfaces::MakeRpc(m_node); }
interfaces::Ipc* ipc() override { return m_ipc.get(); }
bool canListenIpc() override { return true; }
node::NodeContext& m_node;

View file

@ -9,6 +9,7 @@
#include <interfaces/echo.h>
#include <interfaces/mining.h>
#include <interfaces/node.h>
#include <interfaces/rpc.h>
#include <interfaces/wallet.h>
#include <memory>
@ -36,6 +37,7 @@ public:
virtual std::unique_ptr<Mining> makeMining() { return nullptr; }
virtual std::unique_ptr<WalletLoader> makeWalletLoader(Chain& chain) { return nullptr; }
virtual std::unique_ptr<Echo> makeEcho() { return nullptr; }
virtual std::unique_ptr<Rpc> makeRpc() { return nullptr; }
virtual Ipc* ipc() { return nullptr; }
virtual bool canListenIpc() { return false; }
};
@ -53,6 +55,25 @@ std::unique_ptr<Init> MakeWalletInit(int argc, char* argv[], int& exit_status);
//! Return implementation of Init interface for the gui process.
std::unique_ptr<Init> 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<Init> MakeBasicInit(const char* exe_name, const char* process_argv0="");
} // namespace interfaces
#endif // BITCOIN_INTERFACES_INIT_H

31
src/interfaces/rpc.h Normal file
View file

@ -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 <memory>
#include <string>
class UniValue;
namespace node {
struct NodeContext;
} // namespace node
namespace interfaces {
//! Interface giving clients ability to emulate 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<Rpc> MakeRpc(node::NodeContext& node);
} // namespace interfaces
#endif // BITCOIN_INTERFACES_RPC_H

View file

@ -14,6 +14,7 @@ target_capnp_sources(bitcoin_ipc ${PROJECT_SOURCE_DIR}
capnp/echo.capnp
capnp/init.capnp
capnp/mining.capnp
capnp/rpc.capnp
)
target_link_libraries(bitcoin_ipc

View file

@ -7,5 +7,6 @@
#include <ipc/capnp/echo.capnp.proxy-types.h>
#include <ipc/capnp/mining.capnp.proxy-types.h>
#include <ipc/capnp/rpc.capnp.proxy-types.h>
#endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H

View file

@ -15,9 +15,11 @@ $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 @2 (context :Proxy.Context) -> (result :Mining.Mining);
makeRpc @3 (context :Proxy.Context) -> (result :Rpc.Rpc);
}

12
src/ipc/capnp/rpc-types.h Normal file
View file

@ -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 <ipc/capnp/common.capnp.proxy-types.h>
#include <ipc/capnp/common-types.h>
#include <ipc/capnp/rpc.capnp.proxy.h>
#endif // BITCOIN_IPC_CAPNP_RPC_TYPES_H

17
src/ipc/capnp/rpc.capnp Normal file
View file

@ -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);
}

View file

@ -71,10 +71,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);

View file

@ -12,12 +12,14 @@
#include <consensus/validation.h>
#include <deploymentstatus.h>
#include <external_signer.h>
#include <httprpc.h>
#include <index/blockfilterindex.h>
#include <init.h>
#include <interfaces/chain.h>
#include <interfaces/handler.h>
#include <interfaces/mining.h>
#include <interfaces/node.h>
#include <interfaces/rpc.h>
#include <interfaces/types.h>
#include <interfaces/wallet.h>
#include <kernel/chain.h>
@ -80,6 +82,7 @@ using interfaces::Handler;
using interfaces::MakeSignalHandler;
using interfaces::Mining;
using interfaces::Node;
using interfaces::Rpc;
using interfaces::WalletLoader;
using node::BlockAssembler;
using node::BlockWaitOptions;
@ -1115,6 +1118,24 @@ public:
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
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
@ -1122,4 +1143,5 @@ namespace interfaces {
std::unique_ptr<Node> MakeNode(node::NodeContext& context) { return std::make_unique<node::NodeImpl>(context); }
std::unique_ptr<Chain> MakeChain(node::NodeContext& context) { return std::make_unique<node::ChainImpl>(context); }
std::unique_ptr<Mining> MakeMining(node::NodeContext& context) { return std::make_unique<node::MinerImpl>(context); }
std::unique_ptr<Rpc> MakeRpc(node::NodeContext& context) { return std::make_unique<node::RpcImpl>(context); }
} // namespace interfaces

View file

@ -7,7 +7,7 @@
#define BITCOIN_RPC_PROTOCOL_H
//! HTTP status codes
enum HTTPStatusCode
enum HTTPStatusCode : int
{
HTTP_OK = 200,
HTTP_NO_CONTENT = 204,

View file

@ -26,6 +26,7 @@ function(create_test_config)
set_configure_variable(WITH_ZMQ ENABLE_ZMQ)
set_configure_variable(ENABLE_EXTERNAL_SIGNER ENABLE_EXTERNAL_SIGNER)
set_configure_variable(WITH_USDT ENABLE_USDT_TRACEPOINTS)
set_configure_variable(ENABLE_IPC ENABLE_IPC)
configure_file(config.ini.in config.ini USE_SOURCE_PERMISSIONS @ONLY)
endfunction()

View file

@ -26,3 +26,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true
@ENABLE_IPC_TRUE@ENABLE_IPC=true

View file

@ -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
@ -398,6 +399,14 @@ 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_enabled():
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()

View file

@ -0,0 +1,81 @@
#!/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
import tempfile
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):
# Always run IPC node binary
self.binary_paths.bitcoind = self.binary_paths.bitcoin_node
# Work around default CI path exceeding maximum socket path length.
# On Linux sun_path is 108 bytes, on macOS it's only 104. Includes
# null terminator.
self.socket_path = self.options.tmpdir + "/node0/regtest/node.sock"
if len(self.socket_path.encode('utf-8')) < 104:
self.extra_args = [["-ipcbind=unix"]]
self.default_socket = True
else:
self.socket_path = tempfile.mktemp()
self.extra_args = [[f"-ipcbind=unix:{self.socket_path}"]]
self.default_socket = False
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:
assert_equal(result.stdout, error)
else:
assert_equal(result.stdout, '[\n "foo"\n]\n')
assert_equal(result.stderr, None)
assert_equal(result.returncode, 1 if error else 0)
def run_test(self):
if not self.default_socket:
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(self.nodes[0].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"
http_ipc_error = "error: -rpcconnect and -ipcconnect options cannot both be enabled\n"
ipc_connect_error = "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"
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 self.default_socket:
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:{self.socket_path}"], ipc_error)
self.test_cli(["-noipcconnect"], http_error)
self.test_cli(["-ipcconnect=unix", "-rpcconnect=127.0.0.1"], http_ipc_error)
self.stop_node(0)
if __name__ == '__main__':
TestBitcoinIpcCli(__file__).main()

View file

@ -285,13 +285,15 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
"bitcoin-chainstate": ("bitcoinchainstate", "BITCOINCHAINSTATE"),
"bitcoin-wallet": ("bitcoinwallet", "BITCOINWALLET"),
}
for binary, [attribute_name, env_variable_name] in binaries.items():
default_filename = os.path.join(
def binary_path(binary):
return os.path.join(
self.config["environment"]["BUILDDIR"],
"bin",
binary + self.config["environment"]["EXEEXT"],
)
setattr(paths, attribute_name, os.getenv(env_variable_name, default=default_filename))
for binary, [attribute_name, env_variable_name] in binaries.items():
setattr(paths, attribute_name, os.getenv(env_variable_name) or binary_path(binary))
paths.bitcoin_node = binary_path("bitcoin-node")
return paths
def get_binaries(self, bin_dir=None):
@ -1007,6 +1009,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
if not self.is_cli_compiled():
raise SkipTest("bitcoin-cli has not been compiled.")
def skip_if_no_ipc(self):
"""Skip the running test if ipc is not enabled."""
if not self.is_ipc_enabled():
raise SkipTest("ipc is not enabled.")
def skip_if_no_previous_releases(self):
"""Skip the running test if previous releases are not available."""
if not self.has_previous_releases():
@ -1061,5 +1068,9 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
"""Checks whether the wallet module was compiled with BDB support."""
return self.config["components"].getboolean("USE_BDB")
def is_ipc_enabled(self):
"""Checks whether ipc is enabled."""
return self.config["components"].getboolean("ENABLE_IPC")
def has_blockfile(self, node, filenum: str):
return (node.blocks_path/ f"blk{filenum}.dat").is_file()

View file

@ -355,6 +355,7 @@ BASE_SCRIPTS = [
'rpc_help.py',
'p2p_handshake.py',
'p2p_handshake.py --v2transport',
'interface_ipc_cli.py',
'feature_dirsymlinks.py',
'feature_help.py',
'feature_shutdown.py',