Compare commits

...

6 commits

Author SHA1 Message Date
Ryan Ofsky
bf51eef4ae
Merge 9ff5bc7565 into c5e44a0435 2025-04-29 11:51:45 +02:00
merge-script
c5e44a0435
Merge bitcoin/bitcoin#32369: test: Use the correct node for doubled keypath test
Some checks are pending
CI / macOS 14 native, arm64, fuzz (push) Waiting to run
CI / Windows native, VS 2022 (push) Waiting to run
CI / Windows native, fuzz, VS 2022 (push) Waiting to run
CI / Linux->Windows cross, no tests (push) Waiting to run
CI / Windows, test cross-built (push) Blocked by required conditions
CI / ASan + LSan + UBSan + integer, no depends, USDT (push) Waiting to run
CI / test each commit (push) Waiting to run
CI / macOS 14 native, arm64, no depends, sqlite only, gui (push) Waiting to run
32d55e28af test: Use the correct node for doubled keypath test (Ava Chow)

Pull request description:

  #29124 had a silent merge conflict with #32350 which resulted in it using the wrong node. Fix the test to use the correct v22 node.

ACKs for top commit:
  maflcko:
    lgtm ACK 32d55e28af
  rkrux:
    ACK 32d55e28af
  BrandonOdiwuor:
    Code Review ACK 32d55e28af

Tree-SHA512: 1e0231985beb382b16e1d608c874750423d0502388db0c8ad450b22d17f9d96f5e16a6b44948ebda5efc750f62b60d0de8dd20131f449427426a36caf374af92
2025-04-29 09:59:42 +01:00
Ava Chow
32d55e28af test: Use the correct node for doubled keypath test 2025-04-28 14:44:17 -07:00
Ryan Ofsky
9ff5bc7565 test: add interface_ipc_cli.py testing bitcoin-cli -ipcconnect
Co-authored-by: Sjors Provoost <sjors@sprovoost.nl>
2025-04-18 11:37:21 -04:00
Ryan Ofsky
113b46d310 bitcoin-cli: Add -ipcconnect option
This implements an idea from Pieter Wuille <pieter@wuille.net>
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.
2025-04-17 09:40:30 -04:00
Ryan Ofsky
ea98a42640 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.
2025-04-17 09:34:36 -04:00
25 changed files with 408 additions and 87 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',

View file

@ -87,7 +87,7 @@ class BackwardsCompatibilityTest(BitcoinTestFramework):
# 0.21.x and 22.x would both produce bad derivation paths when topping up an inactive hd chain
# Make sure that this is being automatically cleaned up by migration
node_master = self.nodes[1]
node_v22 = self.nodes[self.num_nodes - 5]
node_v22 = self.nodes[self.num_nodes - 3]
wallet_name = "bad_deriv_path"
node_v22.createwallet(wallet_name=wallet_name, descriptors=False)
bad_deriv_wallet = node_v22.get_wallet_rpc(wallet_name)