diff --git a/doc/release-notes.md b/doc/release-notes.md
index 019b7c683..191cb9b34 100644
--- a/doc/release-notes.md
+++ b/doc/release-notes.md
@@ -1,14 +1,18 @@
# Bitcoin ABC 0.23.0 Release Notes
Bitcoin ABC version 0.23.0 is now available from:
This release includes the following features and fixes:
- A `download` permission has been extracted from the `noban` permission. For
compatibility, `noban` implies the `download` permission, but this may change
in future releases. Refer to the help of the affected settings `-whitebind`
and `-whitelist` for more details.
- The `getpeerinfo` RPC now has additional `last_block` and `last_transaction`
fields that return the UNIX epoch time of the last block and the last valid
transaction received from each peer.
+ - A new `bitcoin-cli -netinfo` command provides a network peer connections
+ dashboard that displays data from the `getpeerinfo` and `getnetworkinfo` RPCs
+ in a human-readable format. An optional integer argument from `0` to `4` may
+ be passed to see increasing levels of detail.
diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp
index 7e3cf2cdd..9286f1bd4 100644
--- a/src/bitcoin-cli.cpp
+++ b/src/bitcoin-cli.cpp
@@ -1,771 +1,1053 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2016 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#if defined(HAVE_CONFIG_H)
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
const std::function G_TRANSLATION_FUN = nullptr;
static const char DEFAULT_RPCCONNECT[] = "127.0.0.1";
static const int DEFAULT_HTTP_CLIENT_TIMEOUT = 900;
static const bool DEFAULT_NAMED = false;
static const int CONTINUE_EXECUTION = -1;
+static const std::string ONION{".onion"};
+static const size_t ONION_LEN{ONION.size()};
static void SetupCliArgs(ArgsManager &argsman) {
SetupHelpOptions(argsman);
const auto defaultBaseParams =
CreateBaseChainParams(CBaseChainParams::MAIN);
const auto testnetBaseParams =
CreateBaseChainParams(CBaseChainParams::TESTNET);
const auto regtestBaseParams =
CreateBaseChainParams(CBaseChainParams::REGTEST);
argsman.AddArg("-version", "Print version and exit", ArgsManager::ALLOW_ANY,
OptionsCategory::OPTIONS);
argsman.AddArg(
"-conf=",
strprintf("Specify configuration file. Relative paths will be "
"prefixed by datadir location. (default: %s)",
BITCOIN_CONF_FILENAME),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-datadir=", "Specify data directory",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-getinfo",
"Get general information from the remote server. Note that unlike "
"server-side RPC calls, the results of -getinfo is the result of "
"multiple non-atomic requests. Some entries in the result may "
"represent results from different states (e.g. wallet balance may be "
"as of a different block from the chain state reported)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
+ argsman.AddArg("-netinfo",
+ "Get network peer connection information from the remote "
+ "server. An optional integer argument from 0 to 4 can be "
+ "passed for different peers listings (default: 0).",
+ ArgsManager::ALLOW_INT, OptionsCategory::OPTIONS);
+
SetupChainParamsBaseOptions(argsman);
argsman.AddArg(
"-named",
strprintf("Pass named instead of positional arguments (default: %s)",
DEFAULT_NAMED),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-rpcconnect=",
strprintf("Send commands to node running on (default: %s)",
DEFAULT_RPCCONNECT),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-rpccookiefile=",
"Location of the auth cookie. Relative paths will be prefixed "
"by a net-specific datadir location. (default: data dir)",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcport=",
strprintf("Connect to JSON-RPC on (default: %u, "
"testnet: %u, regtest: %u)",
defaultBaseParams->RPCPort(),
testnetBaseParams->RPCPort(),
regtestBaseParams->RPCPort()),
ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY,
OptionsCategory::OPTIONS);
argsman.AddArg("-rpcwait", "Wait for RPC server to start",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcuser=", "Username for JSON-RPC connections",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcpassword=", "Password for JSON-RPC connections",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg(
"-rpcclienttimeout=",
strprintf("Timeout in seconds during HTTP requests, or 0 for "
"no timeout. (default: %d)",
DEFAULT_HTTP_CLIENT_TIMEOUT),
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-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(
"-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(
"-rpcwallet=",
"Send RPC for non-default wallet on RPC server (needs to exactly match "
"corresponding -wallet option passed to bitcoind). This changes the "
"RPC endpoint used, e.g. http://127.0.0.1:8332/wallet/",
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
}
/** libevent event log callback */
static void libevent_log_cb(int severity, const char *msg) {
#ifndef EVENT_LOG_ERR
// EVENT_LOG_ERR was added in 2.0.19; but before then _EVENT_LOG_ERR existed.
#define EVENT_LOG_ERR _EVENT_LOG_ERR
#endif
// Ignore everything other than errors
if (severity >= EVENT_LOG_ERR) {
throw std::runtime_error(strprintf("libevent error: %s", msg));
}
}
//////////////////////////////////////////////////////////////////////////////
//
// Start
//
//
// Exception thrown on connection error. This error is used to determine when
// to wait if -rpcwait is given.
//
class CConnectionFailed : public std::runtime_error {
public:
explicit inline CConnectionFailed(const std::string &msg)
: std::runtime_error(msg) {}
};
//
// This function returns either one of EXIT_ codes when it's expected to stop
// the process or CONTINUE_EXECUTION when it's expected to continue further.
//
static int AppInitRPC(int argc, char *argv[]) {
//
// Parameters
//
SetupCliArgs(gArgs);
std::string error;
if (!gArgs.ParseParameters(argc, argv, error)) {
tfm::format(std::cerr, "Error parsing command line arguments: %s\n",
error);
return EXIT_FAILURE;
}
if (argc < 2 || HelpRequested(gArgs) || gArgs.IsArgSet("-version")) {
std::string strUsage =
PACKAGE_NAME " RPC client version " + FormatFullVersion() + "\n";
if (!gArgs.IsArgSet("-version")) {
strUsage += "\n"
"Usage: bitcoin-cli [options] [params] "
"Send command to " PACKAGE_NAME "\n"
"or: bitcoin-cli [options] -named "
"[name=value]... Send command to " PACKAGE_NAME
" (with named arguments)\n"
"or: bitcoin-cli [options] help "
"List commands\n"
"or: bitcoin-cli [options] help Get "
"help for a command\n";
strUsage += "\n" + gArgs.GetHelpMessage();
}
tfm::format(std::cout, "%s", strUsage);
if (argc < 2) {
tfm::format(std::cerr, "Error: too few parameters\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
if (!CheckDataDirOption()) {
tfm::format(std::cerr,
"Error: Specified data directory \"%s\" does not exist.\n",
gArgs.GetArg("-datadir", ""));
return EXIT_FAILURE;
}
if (!gArgs.ReadConfigFiles(error, true)) {
tfm::format(std::cerr, "Error reading configuration file: %s\n", error);
return EXIT_FAILURE;
}
// Check for -chain, -testnet or -regtest parameter (BaseParams() calls are
// only valid after this clause)
try {
SelectBaseParams(gArgs.GetChainName());
} catch (const std::exception &e) {
tfm::format(std::cerr, "Error: %s\n", e.what());
return EXIT_FAILURE;
}
return CONTINUE_EXECUTION;
}
/** Reply structure for request_done to fill in */
struct HTTPReply {
HTTPReply() : status(0), error(-1) {}
int status;
int error;
std::string body;
};
static std::string http_errorstring(int code) {
switch (code) {
#if LIBEVENT_VERSION_NUMBER >= 0x02010300
case EVREQ_HTTP_TIMEOUT:
return "timeout reached";
case EVREQ_HTTP_EOF:
return "EOF reached";
case EVREQ_HTTP_INVALID_HEADER:
return "error while reading header, or invalid header";
case EVREQ_HTTP_BUFFER_ERROR:
return "error encountered while reading or writing";
case EVREQ_HTTP_REQUEST_CANCEL:
return "request was canceled";
case EVREQ_HTTP_DATA_TOO_LONG:
return "response body is larger than allowed";
#endif
default:
return "unknown";
}
}
static void http_request_done(struct evhttp_request *req, void *ctx) {
HTTPReply *reply = static_cast(ctx);
if (req == nullptr) {
/**
* If req is nullptr, it means an error occurred while connecting: the
* error code will have been passed to http_error_cb.
*/
reply->status = 0;
return;
}
reply->status = evhttp_request_get_response_code(req);
struct evbuffer *buf = evhttp_request_get_input_buffer(req);
if (buf) {
size_t size = evbuffer_get_length(buf);
const char *data = (const char *)evbuffer_pullup(buf, size);
if (data) {
reply->body = std::string(data, size);
}
evbuffer_drain(buf, size);
}
}
#if LIBEVENT_VERSION_NUMBER >= 0x02010300
static void http_error_cb(enum evhttp_request_error err, void *ctx) {
HTTPReply *reply = static_cast(ctx);
reply->error = err;
}
#endif
/**
* Class that handles the conversion from a command-line to a JSON-RPC request,
* as well as converting back to a JSON object that can be shown as result.
*/
class BaseRequestHandler {
public:
virtual ~BaseRequestHandler() {}
virtual UniValue PrepareRequest(const std::string &method,
const std::vector &args) = 0;
virtual UniValue ProcessReply(const UniValue &batch_in) = 0;
};
/** Process getinfo requests */
class GetinfoRequestHandler : public BaseRequestHandler {
public:
const int ID_NETWORKINFO = 0;
const int ID_BLOCKCHAININFO = 1;
const int ID_WALLETINFO = 2;
const int ID_BALANCES = 3;
/** Create a simulated `getinfo` request. */
UniValue PrepareRequest(const std::string &method,
const std::vector &args) override {
if (!args.empty()) {
throw std::runtime_error("-getinfo takes no arguments");
}
UniValue result(UniValue::VARR);
result.push_back(
JSONRPCRequestObj("getnetworkinfo", NullUniValue, ID_NETWORKINFO));
result.push_back(JSONRPCRequestObj("getblockchaininfo", NullUniValue,
ID_BLOCKCHAININFO));
result.push_back(
JSONRPCRequestObj("getwalletinfo", NullUniValue, ID_WALLETINFO));
result.push_back(
JSONRPCRequestObj("getbalances", NullUniValue, ID_BALANCES));
return result;
}
/** Collect values from the batch and form a simulated `getinfo` reply. */
UniValue ProcessReply(const UniValue &batch_in) override {
UniValue result(UniValue::VOBJ);
const std::vector batch = JSONRPCProcessBatchReply(batch_in);
// Errors in getnetworkinfo() and getblockchaininfo() are fatal, pass
// them on; getwalletinfo() and getbalances are allowed to fail if there
// is no wallet.
if (!batch[ID_NETWORKINFO]["error"].isNull()) {
return batch[ID_NETWORKINFO];
}
if (!batch[ID_BLOCKCHAININFO]["error"].isNull()) {
return batch[ID_BLOCKCHAININFO];
}
result.pushKV("version", batch[ID_NETWORKINFO]["result"]["version"]);
result.pushKV("blocks", batch[ID_BLOCKCHAININFO]["result"]["blocks"]);
result.pushKV("headers", batch[ID_BLOCKCHAININFO]["result"]["headers"]);
result.pushKV(
"verificationprogress",
batch[ID_BLOCKCHAININFO]["result"]["verificationprogress"]);
result.pushKV("timeoffset",
batch[ID_NETWORKINFO]["result"]["timeoffset"]);
result.pushKV("connections",
batch[ID_NETWORKINFO]["result"]["connections"]);
result.pushKV("proxy",
batch[ID_NETWORKINFO]["result"]["networks"][0]["proxy"]);
result.pushKV("difficulty",
batch[ID_BLOCKCHAININFO]["result"]["difficulty"]);
result.pushKV("chain",
UniValue(batch[ID_BLOCKCHAININFO]["result"]["chain"]));
if (!batch[ID_WALLETINFO]["result"].isNull()) {
result.pushKV("keypoolsize",
batch[ID_WALLETINFO]["result"]["keypoolsize"]);
if (!batch[ID_WALLETINFO]["result"]["unlocked_until"].isNull()) {
result.pushKV("unlocked_until",
batch[ID_WALLETINFO]["result"]["unlocked_until"]);
}
result.pushKV("paytxfee",
batch[ID_WALLETINFO]["result"]["paytxfee"]);
}
if (!batch[ID_BALANCES]["result"].isNull()) {
result.pushKV("balance",
batch[ID_BALANCES]["result"]["mine"]["trusted"]);
}
result.pushKV("relayfee", batch[ID_NETWORKINFO]["result"]["relayfee"]);
result.pushKV("warnings", batch[ID_NETWORKINFO]["result"]["warnings"]);
return JSONRPCReplyObj(result, NullUniValue, 1);
}
};
+/** Process netinfo requests */
+class NetinfoRequestHandler : public BaseRequestHandler {
+private:
+ bool IsAddrIPv6(const std::string &addr) const {
+ return !addr.empty() && addr.front() == '[';
+ }
+ bool IsInboundOnion(const std::string &addr_local, int mapped_as) const {
+ return mapped_as == 0 && addr_local.find(ONION) != std::string::npos;
+ }
+ bool IsOutboundOnion(const std::string &addr, int mapped_as) const {
+ const size_t addr_len{addr.size()};
+ const size_t onion_pos{addr.rfind(ONION)};
+ return mapped_as == 0 && onion_pos != std::string::npos &&
+ addr_len > ONION_LEN &&
+ (onion_pos == addr_len - ONION_LEN ||
+ onion_pos == addr.find_last_of(":") - ONION_LEN);
+ }
+ //! Optional user-supplied arg to set dashboard details level
+ uint8_t m_details_level{0};
+ bool DetailsRequested() const {
+ return m_details_level > 0 && m_details_level < 5;
+ }
+ bool IsAddressSelected() const {
+ return m_details_level == 2 || m_details_level == 4;
+ }
+ bool IsVersionSelected() const {
+ return m_details_level == 3 || m_details_level == 4;
+ }
+ enum struct NetType {
+ ipv4,
+ ipv6,
+ onion,
+ };
+ struct Peer {
+ int id;
+ int mapped_as;
+ int version;
+ int64_t conn_time;
+ int64_t last_blck;
+ int64_t last_recv;
+ int64_t last_send;
+ int64_t last_trxn;
+ double min_ping;
+ double ping;
+ std::string addr;
+ std::string sub_version;
+ NetType net_type;
+ bool is_block_relay;
+ bool is_outbound;
+ bool operator<(const Peer &rhs) const {
+ return std::tie(is_outbound, min_ping) <
+ std::tie(rhs.is_outbound, rhs.min_ping);
+ }
+ };
+ std::string NetTypeEnumToString(NetType t) {
+ switch (t) {
+ case NetType::ipv4:
+ return "ipv4";
+ case NetType::ipv6:
+ return "ipv6";
+ case NetType::onion:
+ return "onion";
+ } // no default case, so the compiler can warn about missing cases
+ assert(false);
+ }
+ std::string ChainToString() const {
+ if (gArgs.GetChainName() == CBaseChainParams::TESTNET) {
+ return " testnet";
+ }
+ if (gArgs.GetChainName() == CBaseChainParams::REGTEST) {
+ return " regtest";
+ }
+ return "";
+ }
+
+public:
+ const int ID_PEERINFO = 0;
+ const int ID_NETWORKINFO = 1;
+
+ UniValue PrepareRequest(const std::string &method,
+ const std::vector &args) override {
+ if (!args.empty()) {
+ uint8_t n{0};
+ if (ParseUInt8(args.at(0), &n)) {
+ m_details_level = n;
+ }
+ }
+ UniValue result(UniValue::VARR);
+ result.push_back(
+ JSONRPCRequestObj("getpeerinfo", NullUniValue, ID_PEERINFO));
+ result.push_back(
+ JSONRPCRequestObj("getnetworkinfo", NullUniValue, ID_NETWORKINFO));
+ return result;
+ }
+
+ UniValue ProcessReply(const UniValue &batch_in) override {
+ const std::vector batch{JSONRPCProcessBatchReply(batch_in)};
+ if (!batch[ID_PEERINFO]["error"].isNull()) {
+ return batch[ID_PEERINFO];
+ }
+ if (!batch[ID_NETWORKINFO]["error"].isNull()) {
+ return batch[ID_NETWORKINFO];
+ }
+
+ const UniValue &networkinfo{batch[ID_NETWORKINFO]["result"]};
+ if (networkinfo["version"].get_int() < 230000) {
+ throw std::runtime_error("-netinfo requires bitcoind server to be "
+ "running v0.23.0 and up");
+ }
+
+ // Count peer connection totals, and if DetailsRequested(), store peer
+ // data in a vector of structs.
+ const int64_t time_now{GetSystemTimeInSeconds()};
+ // inbound conn counters
+ int ipv4_i{0}, ipv6_i{0}, onion_i{0}, block_relay_i{0}, total_i{0};
+ // outbound conn counters
+ int ipv4_o{0}, ipv6_o{0}, onion_o{0}, block_relay_o{0}, total_o{0};
+ size_t max_peer_id_length{2}, max_addr_length{0};
+ bool is_asmap_on{false};
+ std::vector peers;
+ const UniValue &getpeerinfo{batch[ID_PEERINFO]["result"]};
+
+ for (const UniValue &peer : getpeerinfo.getValues()) {
+ const std::string addr{peer["addr"].get_str()};
+ const std::string addr_local{
+ peer["addrlocal"].isNull() ? "" : peer["addrlocal"].get_str()};
+ const int mapped_as{
+ peer["mapped_as"].isNull() ? 0 : peer["mapped_as"].get_int()};
+ const bool is_block_relay{!peer["relaytxes"].get_bool()};
+ const bool is_inbound{peer["inbound"].get_bool()};
+ NetType net_type{NetType::ipv4};
+ if (is_inbound) {
+ if (IsAddrIPv6(addr)) {
+ net_type = NetType::ipv6;
+ ++ipv6_i;
+ } else if (IsInboundOnion(addr_local, mapped_as)) {
+ net_type = NetType::onion;
+ ++onion_i;
+ } else {
+ ++ipv4_i;
+ }
+ if (is_block_relay) {
+ ++block_relay_i;
+ }
+ } else {
+ if (IsAddrIPv6(addr)) {
+ net_type = NetType::ipv6;
+ ++ipv6_o;
+ } else if (IsOutboundOnion(addr, mapped_as)) {
+ net_type = NetType::onion;
+ ++onion_o;
+ } else {
+ ++ipv4_o;
+ }
+ if (is_block_relay) {
+ ++block_relay_o;
+ }
+ }
+ if (DetailsRequested()) {
+ // Push data for this peer to the peers vector.
+ const int peer_id{peer["id"].get_int()};
+ const int version{peer["version"].get_int()};
+ const std::string sub_version{peer["subver"].get_str()};
+ const int64_t conn_time{peer["conntime"].get_int64()};
+ const int64_t last_blck{peer["last_block"].get_int64()};
+ const int64_t last_recv{peer["lastrecv"].get_int64()};
+ const int64_t last_send{peer["lastsend"].get_int64()};
+ const int64_t last_trxn{peer["last_transaction"].get_int64()};
+ const double min_ping{
+ peer["minping"].isNull() ? -1 : peer["minping"].get_real()};
+ const double ping{peer["pingtime"].isNull()
+ ? -1
+ : peer["pingtime"].get_real()};
+ peers.push_back({peer_id, mapped_as, version, conn_time,
+ last_blck, last_recv, last_send, last_trxn,
+ min_ping, ping, addr, sub_version, net_type,
+ is_block_relay, !is_inbound});
+ max_peer_id_length =
+ std::max(ToString(peer_id).length(), max_peer_id_length);
+ max_addr_length = std::max(addr.length() + 1, max_addr_length);
+ is_asmap_on |= (mapped_as != 0);
+ }
+ }
+
+ // Generate report header.
+ std::string result{strprintf("%s %s%s - %i%s\n\n", PACKAGE_NAME,
+ FormatFullVersion(), ChainToString(),
+ networkinfo["protocolversion"].get_int(),
+ networkinfo["subversion"].get_str())};
+
+ // Report detailed peer connections list sorted by direction and minimum
+ // ping time.
+ if (DetailsRequested() && !peers.empty()) {
+ std::sort(peers.begin(), peers.end());
+ result += "Peer connections sorted by direction and min ping\n<-> "
+ "relay net mping ping send recv txn blk uptime ";
+ if (is_asmap_on) {
+ result += " asmap ";
+ }
+ result += strprintf("%*s %-*s%s\n", max_peer_id_length, "id",
+ IsAddressSelected() ? max_addr_length : 0,
+ IsAddressSelected() ? "address" : "",
+ IsVersionSelected() ? "version" : "");
+ for (const Peer &peer : peers) {
+ std::string version{ToString(peer.version) + peer.sub_version};
+ result += strprintf(
+ "%3s %5s %5s%6s%7s%5s%5s%5s%5s%7s%*i %*s %-*s%s\n",
+ peer.is_outbound ? "out" : "in",
+ peer.is_block_relay ? "block" : "full",
+ NetTypeEnumToString(peer.net_type),
+ peer.min_ping == -1 ? ""
+ : ToString(round(1000 * peer.min_ping)),
+ peer.ping == -1 ? "" : ToString(round(1000 * peer.ping)),
+ peer.last_send == 0 ? ""
+ : ToString(time_now - peer.last_send),
+ peer.last_recv == 0 ? ""
+ : ToString(time_now - peer.last_recv),
+ peer.last_trxn == 0
+ ? ""
+ : ToString((time_now - peer.last_trxn) / 60),
+ peer.last_blck == 0
+ ? ""
+ : ToString((time_now - peer.last_blck) / 60),
+ peer.conn_time == 0
+ ? ""
+ : ToString((time_now - peer.conn_time) / 60),
+ // variable spacing
+ is_asmap_on ? 7 : 0,
+ is_asmap_on && peer.mapped_as != 0
+ ? ToString(peer.mapped_as)
+ : "",
+ // variable spacing
+ max_peer_id_length, peer.id,
+ // variable spacing
+ IsAddressSelected() ? max_addr_length : 0,
+ IsAddressSelected() ? peer.addr : "",
+ IsVersionSelected() && version != "0" ? version : "");
+ }
+ result +=
+ " ms ms sec sec min min min\n\n";
+ }
+
+ // Report peer connection totals by type.
+ total_i = ipv4_i + ipv6_i + onion_i;
+ total_o = ipv4_o + ipv6_o + onion_o;
+ result += " ipv4 ipv6 onion total block-relay\n";
+ result += strprintf("in %5i %5i %5i %5i %5i\n", ipv4_i,
+ ipv6_i, onion_i, total_i, block_relay_i);
+ result += strprintf("out %5i %5i %5i %5i %5i\n", ipv4_o,
+ ipv6_o, onion_o, total_o, block_relay_o);
+ result += strprintf("total %5i %5i %5i %5i %5i\n",
+ ipv4_i + ipv4_o, ipv6_i + ipv6_o, onion_i + onion_o,
+ total_i + total_o, block_relay_i + block_relay_o);
+
+ // Report local addresses, ports, and scores.
+ result += "\nLocal addresses";
+ const UniValue &local_addrs{networkinfo["localaddresses"]};
+ if (local_addrs.empty()) {
+ result += ": n/a\n";
+ } else {
+ for (const UniValue &addr : local_addrs.getValues()) {
+ result +=
+ strprintf("\n%-40i port %5i score %6i",
+ addr["address"].get_str(), addr["port"].get_int(),
+ addr["score"].get_int());
+ }
+ }
+
+ return JSONRPCReplyObj(UniValue{result}, NullUniValue, 1);
+ }
+};
+
/** Process default single requests */
class DefaultRequestHandler : public BaseRequestHandler {
public:
UniValue PrepareRequest(const std::string &method,
const std::vector &args) override {
UniValue params;
if (gArgs.GetBoolArg("-named", DEFAULT_NAMED)) {
params = RPCConvertNamedValues(method, args);
} else {
params = RPCConvertValues(method, args);
}
return JSONRPCRequestObj(method, params, 1);
}
UniValue ProcessReply(const UniValue &reply) override {
return reply.get_obj();
}
};
static UniValue CallRPC(BaseRequestHandler *rh, const std::string &strMethod,
const std::vector &args,
const std::optional &rpcwallet = {}) {
std::string host;
// In preference order, we choose the following for the port:
// 1. -rpcport
// 2. port in -rpcconnect (ie following : in ipv4 or ]: in ipv6)
// 3. default port for chain
int port = BaseParams().RPCPort();
SplitHostPort(gArgs.GetArg("-rpcconnect", DEFAULT_RPCCONNECT), port, host);
port = gArgs.GetArg("-rpcport", port);
// Obtain event base
raii_event_base base = obtain_event_base();
// Synchronously look up hostname
raii_evhttp_connection evcon =
obtain_evhttp_connection_base(base.get(), host, port);
// Set connection timeout
{
const int timeout =
gArgs.GetArg("-rpcclienttimeout", DEFAULT_HTTP_CLIENT_TIMEOUT);
if (timeout > 0) {
evhttp_connection_set_timeout(evcon.get(), timeout);
} else {
// Indefinite request timeouts are not possible in libevent-http,
// so we set the timeout to a very long time period instead.
// Average length of year in Gregorian calendar
constexpr int YEAR_IN_SECONDS = 31556952;
evhttp_connection_set_timeout(evcon.get(), 5 * YEAR_IN_SECONDS);
}
}
HTTPReply response;
raii_evhttp_request req =
obtain_evhttp_request(http_request_done, (void *)&response);
if (req == nullptr) {
throw std::runtime_error("create http request failed");
}
#if LIBEVENT_VERSION_NUMBER >= 0x02010300
evhttp_request_set_error_cb(req.get(), http_error_cb);
#endif
// Get credentials
std::string strRPCUserColonPass;
bool failedToGetAuthCookie = false;
if (gArgs.GetArg("-rpcpassword", "") == "") {
// Try fall back to cookie-based authentication if no password is
// provided
if (!GetAuthCookie(&strRPCUserColonPass)) {
failedToGetAuthCookie = true;
}
} else {
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" +
gArgs.GetArg("-rpcpassword", "");
}
struct evkeyvalq *output_headers =
evhttp_request_get_output_headers(req.get());
assert(output_headers);
evhttp_add_header(output_headers, "Host", host.c_str());
evhttp_add_header(output_headers, "Connection", "close");
evhttp_add_header(
output_headers, "Authorization",
(std::string("Basic ") + EncodeBase64(strRPCUserColonPass)).c_str());
// Attach request data
std::string strRequest = rh->PrepareRequest(strMethod, args).write() + "\n";
struct evbuffer *output_buffer =
evhttp_request_get_output_buffer(req.get());
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());
// ownership moved to evcon in above call
req.release();
if (r != 0) {
throw CConnectionFailed("send http request failed");
}
event_base_dispatch(base.get());
if (response.status == 0) {
std::string responseErrorMessage;
if (response.error != -1) {
responseErrorMessage =
strprintf(" (error code %d - \"%s\")", response.error,
http_errorstring(response.error));
}
throw CConnectionFailed(
strprintf("Could not connect to the server %s:%d%s\n\nMake sure "
"the bitcoind server is running and that you are "
"connecting to the correct RPC port.",
host, port, responseErrorMessage));
} else if (response.status == HTTP_UNAUTHORIZED) {
if (failedToGetAuthCookie) {
throw std::runtime_error(strprintf(
"Could not locate RPC credentials. No authentication cookie "
"could be found, and RPC password is not set. See "
"-rpcpassword and -stdinrpcpass. Configuration file: (%s)",
GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME))
.string()));
} else {
throw std::runtime_error(
"Authorization failed: Incorrect rpcuser or rpcpassword");
}
} else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST &&
response.status != HTTP_NOT_FOUND &&
response.status != HTTP_INTERNAL_SERVER_ERROR) {
throw std::runtime_error(
strprintf("server returned HTTP error %d", response.status));
} else if (response.body.empty()) {
throw std::runtime_error("no response from server");
}
// Parse reply
UniValue valReply(UniValue::VSTR);
if (!valReply.read(response.body)) {
throw std::runtime_error("couldn't parse reply from server");
}
const UniValue reply = rh->ProcessReply(valReply);
if (reply.empty()) {
throw std::runtime_error(
"expected reply to have result, error and id properties");
}
return reply;
}
/**
* ConnectAndCallRPC wraps CallRPC with -rpcwait and an exception handler.
*
* @param[in] rh Pointer to RequestHandler.
* @param[in] strMethod Reference to const string method to forward to CallRPC.
* @param[in] rpcwallet Reference to const optional string wallet name to
* forward to CallRPC.
* @returns the RPC response as a UniValue object.
* @throws a CConnectionFailed std::runtime_error if connection failed or RPC
* server still in warmup.
*/
static UniValue
ConnectAndCallRPC(BaseRequestHandler *rh, const std::string &strMethod,
const std::vector &args,
const std::optional &rpcwallet = {}) {
UniValue response(UniValue::VOBJ);
// Execute and handle connection failures with -rpcwait.
const bool fWait = gArgs.GetBoolArg("-rpcwait", false);
do {
try {
response = CallRPC(rh, strMethod, args, rpcwallet);
if (fWait) {
const UniValue &error = find_value(response, "error");
if (!error.isNull() &&
error["code"].get_int() == RPC_IN_WARMUP) {
throw CConnectionFailed("server in warmup");
}
}
break; // Connection succeeded, no need to retry.
} catch (const CConnectionFailed &) {
if (fWait) {
UninterruptibleSleep(std::chrono::milliseconds{1000});
} else {
throw;
}
}
} while (fWait);
return response;
}
/**
* GetWalletBalances calls listwallets; if more than one wallet is loaded, it
* then fetches mine.trusted balances for each loaded wallet and pushes them to
* `result`.
*
* @param result Reference to UniValue object the wallet names and balances are
* pushed to.
*/
[[maybe_unused]] static void GetWalletBalances(UniValue &result) {
std::unique_ptr rh{
std::make_unique()};
const UniValue listwallets =
ConnectAndCallRPC(rh.get(), "listwallets", /* args=*/{});
if (!find_value(listwallets, "error").isNull()) {
return;
}
const UniValue &wallets = find_value(listwallets, "result");
if (wallets.size() <= 1) {
return;
}
UniValue balances(UniValue::VOBJ);
for (const UniValue &wallet : wallets.getValues()) {
const std::string wallet_name = wallet.get_str();
const UniValue getbalances = ConnectAndCallRPC(
rh.get(), "getbalances", /* args=*/{}, wallet_name);
const UniValue &balance =
find_value(getbalances, "result")["mine"]["trusted"];
balances.pushKV(wallet_name, balance);
}
result.pushKV("balances", balances);
}
static int CommandLineRPC(int argc, char *argv[]) {
std::string strPrint;
int nRet = 0;
try {
// Skip switches
while (argc > 1 && IsSwitchChar(argv[1][0])) {
argc--;
argv++;
}
std::string rpcPass;
if (gArgs.GetBoolArg("-stdinrpcpass", false)) {
NO_STDIN_ECHO();
if (!StdinReady()) {
fputs("RPC password> ", stderr);
fflush(stderr);
}
if (!std::getline(std::cin, rpcPass)) {
throw std::runtime_error("-stdinrpcpass specified but failed "
"to read from standard input");
}
if (StdinTerminal()) {
fputc('\n', stdout);
}
gArgs.ForceSetArg("-rpcpassword", rpcPass);
}
std::vector args =
std::vector(&argv[1], &argv[argc]);
if (gArgs.GetBoolArg("-stdinwalletpassphrase", false)) {
NO_STDIN_ECHO();
std::string walletPass;
if (args.size() < 1 ||
args[0].substr(0, 16) != "walletpassphrase") {
throw std::runtime_error(
"-stdinwalletpassphrase is only applicable for "
"walletpassphrase(change)");
}
if (!StdinReady()) {
fputs("Wallet passphrase> ", stderr);
fflush(stderr);
}
if (!std::getline(std::cin, walletPass)) {
throw std::runtime_error("-stdinwalletpassphrase specified but "
"failed to read from standard input");
}
if (StdinTerminal()) {
fputc('\n', stdout);
}
args.insert(args.begin() + 1, walletPass);
}
if (gArgs.GetBoolArg("-stdin", false)) {
// Read one arg per line from stdin and append
std::string line;
while (std::getline(std::cin, line)) {
args.push_back(line);
}
if (StdinTerminal()) {
fputc('\n', stdout);
}
}
std::unique_ptr rh;
std::string method;
if (gArgs.IsArgSet("-getinfo")) {
rh.reset(new GetinfoRequestHandler());
+ } else if (gArgs.GetBoolArg("-netinfo", false)) {
+ rh.reset(new NetinfoRequestHandler());
} else {
rh.reset(new DefaultRequestHandler());
if (args.size() < 1) {
throw std::runtime_error(
"too few parameters (need at least command)");
}
method = args[0];
// Remove trailing method name from arguments vector
args.erase(args.begin());
}
std::optional wallet_name{};
if (gArgs.IsArgSet("-rpcwallet")) {
wallet_name = gArgs.GetArg("-rpcwallet", "");
}
const UniValue reply =
ConnectAndCallRPC(rh.get(), method, args, wallet_name);
// Parse reply
UniValue result = find_value(reply, "result");
const UniValue &error = find_value(reply, "error");
if (!error.isNull()) {
// Error
strPrint = "error: " + error.write();
nRet = abs(error["code"].get_int());
if (error.isObject()) {
const UniValue &errCode = find_value(error, "code");
const UniValue &errMsg = find_value(error, "message");
strPrint = errCode.isNull()
? ""
: ("error code: " + errCode.getValStr() + "\n");
if (errMsg.isStr()) {
strPrint += ("error message:\n" + errMsg.get_str());
}
if (errCode.isNum() &&
errCode.get_int() == RPC_WALLET_NOT_SPECIFIED) {
strPrint += "\nTry adding \"-rpcwallet=\" option "
"to bitcoin-cli command line.";
}
}
} else {
if (gArgs.IsArgSet("-getinfo") && !gArgs.IsArgSet("-rpcwallet")) {
// fetch multiwallet balances and append to result
GetWalletBalances(result);
}
// Result
if (result.isNull()) {
strPrint = "";
} else if (result.isStr()) {
strPrint = result.get_str();
} else {
strPrint = result.write(2);
}
}
} catch (const std::exception &e) {
strPrint = std::string("error: ") + e.what();
nRet = EXIT_FAILURE;
} catch (...) {
PrintExceptionContinue(nullptr, "CommandLineRPC()");
throw;
}
if (strPrint != "") {
tfm::format(nRet == 0 ? std::cout : std::cerr, "%s\n", strPrint);
}
return nRet;
}
int main(int argc, char *argv[]) {
#ifdef WIN32
util::WinCmdLineArgs winArgs;
std::tie(argc, argv) = winArgs.get();
#endif
SetupEnvironment();
if (!SetupNetworking()) {
tfm::format(std::cerr, "Error: Initializing networking failed\n");
return EXIT_FAILURE;
}
event_set_log_callback(&libevent_log_cb);
try {
int ret = AppInitRPC(argc, argv);
if (ret != CONTINUE_EXECUTION) {
return ret;
}
} catch (const std::exception &e) {
PrintExceptionContinue(&e, "AppInitRPC()");
return EXIT_FAILURE;
} catch (...) {
PrintExceptionContinue(nullptr, "AppInitRPC()");
return EXIT_FAILURE;
}
int ret = EXIT_FAILURE;
try {
ret = CommandLineRPC(argc, argv);
} catch (const std::exception &e) {
PrintExceptionContinue(&e, "CommandLineRPC()");
} catch (...) {
PrintExceptionContinue(nullptr, "CommandLineRPC()");
}
return ret;
}