Merge #6034: backport: merge bitcoin#21261, #20877, #21832, #22547, #22544, #22959, #23324, partial bitcoin#20764 (cli backports: part 2)

bde72a41fe merge bitcoin#23324: print peer counts for all reachable networks in -netinfo (Kittywhiskers Van Gogh)
4b245441a0 merge bitcoin#22959: Display all proxies in -getinfo (Kittywhiskers Van Gogh)
30b0fcf4a6 merge bitcoin#22544: drop torv2; torv3 becomes onion per GetNetworkName() (Kittywhiskers Van Gogh)
b6ca36edda merge bitcoin#22547: Add progress bar for -getinfo (Kittywhiskers Van Gogh)
1f89bfd176 merge bitcoin#21832: Implement human readable -getinfo (Kittywhiskers Van Gogh)
2200b78a15 merge bitcoin#20877: user help and argument parsing improvements (Kittywhiskers Van Gogh)
bd934c71eb partial bitcoin#20764: cli -netinfo peer connections dashboard updates (Kittywhiskers Van Gogh)
b2d865633f merge bitcoin#21261: update inbound eviction protection for multiple networks, add I2P peers (Kittywhiskers Van Gogh)
0b16b50fcb cli: fix loop counter comparison in `ProcessReply` (Kittywhiskers Van Gogh)

Pull request description:

  ## Additional Information

  * Dependency for https://github.com/dashpay/dash/pull/6035

  * Dependency for https://github.com/dashpay/dash/pull/6031

  * In [dash#5904](https://github.com/dashpay/dash/pull/5904) ([bitcoin#21595](https://github.com/bitcoin/bitcoin/pull/21595)), one of the loops in `ProcessReply` is supposed to iterate `rows.size()` times (which at the time was hardcoded to `3`), the backport erroneously set the value to `m_networks.size()` (which also evaluated to `3`) as part of increasing `m_networks.size()` usage.

    As this pull request includes [bitcoin#23324](https://github.com/bitcoin/bitcoin/pull/23324), which changes it over to  `rows.size()`, the above has been corrected in a separate commit for documentation purposes.

  * `-addrinfo` output

    ![dash-cli addrinfo output](https://github.com/dashpay/dash/assets/63189531/24db46be-729e-4fa8-a268-87f2497cff9a)

  * `-getinfo` output (diamonds are due to rendering limitations of my terminal and are not indicative of the symbols used)

    ![dash-cli getinfo output](https://github.com/dashpay/dash/assets/63189531/626fe67f-f505-4a04-931a-76e75146e5a0)

  * `-netinfo` output

    ![dash-cli netinfo output](https://github.com/dashpay/dash/assets/63189531/afbff3d0-7127-44e2-bfe7-81b08c0e214e)

  ## Breaking Changes

  * CLI `-addrinfo` now returns a single field for the number of `onion` addresses known to the node instead of separate `torv2` and `torv3` fields, as support for TorV2 addresses was removed from Dash Core in 18.0.

  * `-getinfo` has been updated to return data in a user-friendly format that also reduces vertical space.

  ## Checklist

  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas **(note: N/A)**
  - [x] I have added or updated relevant unit/integration/functional/e2e tests
  - [x] I have made corresponding changes to the documentation **(note: N/A)**
  - [x] I have assigned this pull request to a milestone

ACKs for top commit:
  PastaPastaPasta:
    utACK bde72a41fe

Tree-SHA512: 921cb45b7e243a321a32c835eb23d5ba8df610ff234a548a9051436a2c21845ce70097fb9a9bb812b77b04373f9f0a9f90264168d97b08da1890be06bfd9f99c
This commit is contained in:
pasta 2024-05-28 12:39:31 -05:00
commit 3b3b1b8e00
No known key found for this signature in database
GPG Key ID: 52527BEDABE87984
10 changed files with 868 additions and 328 deletions

View File

@ -0,0 +1,4 @@
Tools and Utilities
-------------------
- Update `-getinfo` to return data in a user-friendly format that also reduces vertical space.

View File

@ -0,0 +1,6 @@
Tools and Utilities
-------------------
- CLI `-addrinfo` now returns a single field for the number of `onion` addresses
known to the node instead of separate `torv2` and `torv3` fields, as support
for Tor V2 addresses was removed from Dash Core in 18.0.

View File

@ -18,10 +18,9 @@ There are several ways to see your local onion address in Dash Core:
You may set the `-debug=tor` config logging option to have additional
information in the debug log about your Tor configuration.
CLI `-addrinfo` returns the number of addresses known to your node per network
type, including Tor v2 and v3. This is useful to see how many onion addresses
are known to your node for `-onlynet=onion` and how many Tor v3 addresses it
knows when upgrading to current and future Tor releases that support Tor v3 only.
CLI `-addrinfo` returns the number of addresses known to your node per
network. This can be useful to see how many onion peers your node knows,
e.g. for `-onlynet=onion`.
## 1. Run Dash Core behind a Tor proxy

View File

@ -11,6 +11,7 @@
#include <chainparamsbase.h>
#include <clientversion.h>
#include <compat.h>
#include <policy/feerate.h>
#include <rpc/client.h>
#include <rpc/mining.h>
#include <rpc/protocol.h>
@ -31,6 +32,10 @@
#include <string>
#include <tuple>
#ifndef WIN32
#include <unistd.h>
#endif
#include <event2/buffer.h>
#include <event2/keyvalq_struct.h>
#include <support/events.h>
@ -51,6 +56,9 @@ static constexpr int8_t UNKNOWN_NETWORK{-1};
/** Default number of blocks to generate for RPC generatetoaddress. */
static const std::string DEFAULT_NBLOCKS = "1";
/** Default -color setting. */
static const std::string DEFAULT_COLOR_SETTING{"auto"};
static void SetupCliArgs(ArgsManager& argsman)
{
SetupHelpOptions(argsman);
@ -66,6 +74,8 @@ static void SetupCliArgs(ArgsManager& argsman)
argsman.AddArg("-addrinfo", "Get the number of addresses known to the node, per network and total.", 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). Pass \"help\" for detailed help documentation.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-color=<when>", strprintf("Color setting for CLI output (default: %s). Valid values: always, auto (add color codes when standard output is connected to a terminal and OS is not WIN32), never.", DEFAULT_COLOR_SETTING), ArgsManager::ALLOW_STRING, OptionsCategory::OPTIONS);
argsman.AddArg("-named", strprintf("Pass named instead of positional arguments (default: %s)", DEFAULT_NAMED), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcclienttimeout=<n>", strprintf("Timeout in seconds during HTTP requests, or 0 for no timeout. (default: %d)", DEFAULT_HTTP_CLIENT_TIMEOUT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-rpcconnect=<ip>", strprintf("Send commands to node running on <ip> (default: %s)", DEFAULT_RPCCONNECT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@ -249,7 +259,7 @@ public:
class AddrinfoRequestHandler : public BaseRequestHandler
{
private:
static constexpr std::array m_networks{"ipv4", "ipv6", "torv2", "torv3", "i2p"};
static constexpr std::array m_networks{"ipv4", "ipv6", "onion", "i2p"};
int8_t NetworkStringToId(const std::string& str) const
{
for (size_t i = 0; i < m_networks.size(); ++i) {
@ -275,13 +285,10 @@ public:
if (!nodes.empty() && nodes.at(0)["network"].isNull()) {
throw std::runtime_error("-addrinfo requires dashd server to be running v21.0 and up");
}
// Count the number of peers we know by network, including torv2 versus torv3.
// Count the number of peers known to our node, by network.
std::array<uint64_t, m_networks.size()> counts{{}};
for (const UniValue& node : nodes) {
std::string network_name{node["network"].get_str()};
if (network_name == "onion") {
network_name = node["address"].get_str().size() > 22 ? "torv3" : "torv2";
}
const int8_t network_id{NetworkStringToId(network_name)};
if (network_id == UNKNOWN_NETWORK) continue;
++counts.at(network_id);
@ -350,12 +357,14 @@ public:
connections.pushKV("mn_total", batch[ID_NETWORKINFO]["result"]["connections_mn"]);
result.pushKV("connections", connections);
result.pushKV("proxy", batch[ID_NETWORKINFO]["result"]["networks"][0]["proxy"]);
result.pushKV("networks", batch[ID_NETWORKINFO]["result"]["networks"]);
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("coinjoin_balance", batch[ID_WALLETINFO]["result"]["coinjoin_balance"]);
result.pushKV("has_wallet", true);
result.pushKV("keypoolsize", batch[ID_WALLETINFO]["result"]["keypoolsize"]);
result.pushKV("walletname", batch[ID_WALLETINFO]["result"]["walletname"]);
if (!batch[ID_WALLETINFO]["result"]["unlocked_until"].isNull()) {
result.pushKV("unlocked_until", batch[ID_WALLETINFO]["result"]["unlocked_until"]);
}
@ -375,8 +384,10 @@ class NetinfoRequestHandler : public BaseRequestHandler
{
private:
static constexpr uint8_t MAX_DETAIL_LEVEL{4};
static constexpr std::array m_networks{"ipv4", "ipv6", "onion"};
std::array<std::array<uint16_t, m_networks.size() + 2>, 3> m_counts{{{}}}; //!< Peer counts by (in/out/total, networks/total/block-relay)
static constexpr std::array m_networks{"ipv4", "ipv6", "onion", "i2p"};
std::array<std::array<uint16_t, m_networks.size() + 1>, 3> m_counts{{{}}}; //!< Peer counts by (in/out/total, networks/total)
uint8_t m_block_relay_peers_count{0};
uint8_t m_manual_peers_count{0};
int8_t NetworkStringToId(const std::string& str) const
{
for (size_t i = 0; i < m_networks.size(); ++i) {
@ -384,8 +395,7 @@ private:
}
return UNKNOWN_NETWORK;
}
uint8_t m_details_level{0}; //!< Optional user-supplied arg to set dashboard details level
bool m_is_help_requested{false}; //!< Optional user-supplied arg to print help documentation
uint8_t m_details_level{0}; //!< Optional user-supplied arg to set dashboard details level
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; }
@ -396,6 +406,7 @@ private:
struct Peer {
std::string addr;
std::string sub_version;
std::string conn_type;
std::string network;
std::string age;
double min_ping;
@ -425,61 +436,13 @@ private:
const double milliseconds{round(1000 * seconds)};
return milliseconds > 999999 ? "-" : ToString(milliseconds);
}
const UniValue NetinfoHelp()
std::string ConnectionTypeForNetinfo(const std::string& conn_type) const
{
return std::string{
"-netinfo level|\"help\" \n\n"
"Returns a network peer connections dashboard with information from the remote server.\n"
"Under the hood, -netinfo fetches the data by calling getpeerinfo and getnetworkinfo.\n"
"An optional integer argument from 0 to 4 can be passed for different peers listings.\n"
"Pass \"help\" to see this detailed help documentation.\n"
"If more than one argument is passed, only the first one is read and parsed.\n"
"Suggestion: use with the Linux watch(1) command for a live dashboard; see example below.\n\n"
"Arguments:\n"
"1. level (integer 0-4, optional) Specify the info level of the peers dashboard (default 0):\n"
" 0 - Connection counts and local addresses\n"
" 1 - Like 0 but with a peers listing (without address or version columns)\n"
" 2 - Like 1 but with an address column\n"
" 3 - Like 1 but with a version column\n"
" 4 - Like 1 but with both address and version columns\n"
"2. help (string \"help\", optional) Print this help documentation instead of the dashboard.\n\n"
"Result:\n\n"
"* The peers listing in levels 1-4 displays all of the peers sorted by direction and minimum ping time:\n\n"
" Column Description\n"
" ------ -----------\n"
" <-> Direction\n"
" \"in\" - inbound connections are those initiated by the peer\n"
" \"out\" - outbound connections are those initiated by us\n"
" type Type of peer connection\n"
" \"full\" - full relay, the default\n"
" \"block\" - block relay; like full relay but does not relay transactions or addresses\n"
" net Network the peer connected through (\"ipv4\", \"ipv6\", \"onion\", \"i2p\", or \"cjdns\")\n"
" mping Minimum observed ping time, in milliseconds (ms)\n"
" ping Last observed ping time, in milliseconds (ms)\n"
" send Time since last message sent to the peer, in seconds\n"
" recv Time since last message received from the peer, in seconds\n"
" txn Time since last novel transaction received from the peer and accepted into our mempool, in minutes\n"
" blk Time since last novel block passing initial validity checks received from the peer, in minutes\n"
" age Duration of connection to the peer, in minutes\n"
" asmap Mapped AS (Autonomous System) number in the BGP route to the peer, used for diversifying\n"
" peer selection (only displayed if the -asmap config option is set)\n"
" id Peer index, in increasing order of peer connections since node startup\n"
" address IP address and port of the peer\n"
" version Peer version and subversion concatenated, e.g. \"70016/Satoshi:21.0.0/\"\n\n"
"* The connection counts table displays the number of peers by direction, network, and the totals\n"
" for each, as well as a column for block relay peers.\n\n"
"* The local addresses table lists each local address broadcast by the node, the port, and the score.\n\n"
"Examples:\n\n"
"Connection counts and local addresses only\n"
"> dash-cli -netinfo\n\n"
"Compact peers listing\n"
"> dash-cli -netinfo 1\n\n"
"Full dashboard\n"
"> dash-cli -netinfo 4\n\n"
"Full live dashboard, adjust --interval or --no-title as needed (Linux)\n"
"> watch --interval 1 --no-title dash-cli -netinfo 4\n\n"
"See this help\n"
"> dash-cli -netinfo help\n"};
if (conn_type == "outbound-full-relay") return "full";
if (conn_type == "block-relay-only") return "block";
if (conn_type == "manual" || conn_type == "feeler") return conn_type;
if (conn_type == "addr-fetch") return "addr";
return "";
}
const int64_t m_time_now{GetTimeSeconds()};
@ -493,10 +456,8 @@ public:
uint8_t n{0};
if (ParseUInt8(args.at(0), &n)) {
m_details_level = std::min(n, MAX_DETAIL_LEVEL);
} else if (args.at(0) == "help") {
m_is_help_requested = true;
} else {
throw std::runtime_error(strprintf("invalid -netinfo argument: %s", args.at(0)));
throw std::runtime_error(strprintf("invalid -netinfo argument: %s\nFor more information, run: dash-cli -netinfo help", args.at(0)));
}
}
UniValue result(UniValue::VARR);
@ -507,9 +468,6 @@ public:
UniValue ProcessReply(const UniValue& batch_in) override
{
if (m_is_help_requested) {
return JSONRPCReplyObj(NetinfoHelp(), NullUniValue, 1);
}
const std::vector<UniValue> batch{JSONRPCProcessBatchReply(batch_in)};
if (!batch[ID_PEERINFO]["error"].isNull()) return batch[ID_PEERINFO];
if (!batch[ID_NETWORKINFO]["error"].isNull()) return batch[ID_NETWORKINFO];
@ -526,14 +484,13 @@ public:
if (network_id == UNKNOWN_NETWORK) continue;
const bool is_outbound{!peer["inbound"].get_bool()};
const bool is_block_relay{peer["relaytxes"].isNull() ? false : !peer["relaytxes"].get_bool()};
const std::string conn_type{peer["connection_type"].get_str()};
++m_counts.at(is_outbound).at(network_id); // in/out by network
++m_counts.at(is_outbound).at(m_networks.size()); // in/out overall
++m_counts.at(2).at(network_id); // total by network
++m_counts.at(2).at(m_networks.size()); // total overall
if (is_block_relay) {
++m_counts.at(is_outbound).at(m_networks.size() + 1); // in/out block-relay
++m_counts.at(2).at(m_networks.size() + 1); // total block-relay
}
if (is_block_relay) ++m_block_relay_peers_count;
if (conn_type == "manual") ++m_manual_peers_count;
if (DetailsRequested()) {
// Push data for this peer to the peers vector.
const int peer_id{peer["id"].get_int()};
@ -549,7 +506,7 @@ public:
const std::string addr{peer["addr"].get_str()};
const std::string age{conn_time == 0 ? "" : ToString((m_time_now - conn_time) / 60)};
const std::string sub_version{peer["subver"].get_str()};
m_peers.push_back({addr, sub_version, network, age, min_ping, ping, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_block_relay, is_outbound});
m_peers.push_back({addr, sub_version, conn_type, network, age, min_ping, ping, last_blck, last_recv, last_send, last_trxn, peer_id, mapped_as, version, is_block_relay, is_outbound});
m_max_addr_length = std::max(addr.length() + 1, m_max_addr_length);
m_max_age_length = std::max(age.length(), m_max_age_length);
m_max_id_length = std::max(ToString(peer_id).length(), m_max_id_length);
@ -563,15 +520,15 @@ public:
// Report detailed peer connections list sorted by direction and minimum ping time.
if (DetailsRequested() && !m_peers.empty()) {
std::sort(m_peers.begin(), m_peers.end());
result += strprintf("<-> relay net mping ping send recv txn blk %*s ", m_max_age_length, "age");
result += strprintf("<-> type net mping ping send recv txn blk %*s ", m_max_age_length, "age");
if (m_is_asmap_on) result += " asmap ";
result += strprintf("%*s %-*s%s\n", m_max_id_length, "id", IsAddressSelected() ? m_max_addr_length : 0, IsAddressSelected() ? "address" : "", IsVersionSelected() ? "version" : "");
for (const Peer& peer : m_peers) {
std::string version{ToString(peer.version) + peer.sub_version};
result += strprintf(
"%3s %5s %5s%7s%7s%5s%5s%5s%5s %*s%*i %*s %-*s%s\n",
"%3s %6s %5s%7s%7s%5s%5s%5s%5s %*s%*i %*s %-*s%s\n",
peer.is_outbound ? "out" : "in",
peer.is_block_relay ? "block" : "full",
ConnectionTypeForNetinfo(peer.conn_type),
peer.network,
PingTimeToString(peer.min_ping),
PingTimeToString(peer.ping),
@ -589,18 +546,39 @@ public:
IsAddressSelected() ? peer.addr : "",
IsVersionSelected() && version != "0" ? version : "");
}
result += strprintf(" ms ms sec sec min min %*s\n\n", m_max_age_length, "min");
result += strprintf(" ms ms sec sec min min %*s\n\n", m_max_age_length, "min");
}
// Report peer connection totals by type.
result += " ipv4 ipv6 onion total block-relay\n";
result += " ";
std::vector<int8_t> reachable_networks;
for (const UniValue& network : networkinfo["networks"].getValues()) {
if (network["reachable"].get_bool()) {
const std::string& network_name{network["name"].get_str()};
const int8_t network_id{NetworkStringToId(network_name)};
if (network_id == UNKNOWN_NETWORK) continue;
result += strprintf("%8s", network_name); // column header
reachable_networks.push_back(network_id);
}
};
result += " total block";
if (m_manual_peers_count) result += " manual";
const std::array rows{"in", "out", "total"};
for (uint8_t i = 0; i < m_networks.size(); ++i) {
result += strprintf("%-5s %5i %5i %5i %5i %5i\n", rows.at(i), m_counts.at(i).at(0), m_counts.at(i).at(1), m_counts.at(i).at(2), m_counts.at(i).at(m_networks.size()), m_counts.at(i).at(m_networks.size() + 1));
for (size_t i = 0; i < rows.size(); ++i) {
result += strprintf("\n%-5s", rows[i]); // row header
for (int8_t n : reachable_networks) {
result += strprintf("%8i", m_counts.at(i).at(n)); // network peers count
}
result += strprintf(" %5i", m_counts.at(i).at(m_networks.size())); // total peers count
if (i == 1) { // the outbound row has two extra columns for block relay and manual peer counts
result += strprintf(" %5i", m_block_relay_peers_count);
if (m_manual_peers_count) result += strprintf(" %5i", m_manual_peers_count);
}
}
// Report local addresses, ports, and scores.
result += "\nLocal addresses";
result += "\n\nLocal addresses";
const std::vector<UniValue>& local_addrs{networkinfo["localaddresses"].getValues()};
if (local_addrs.empty()) {
result += ": n/a\n";
@ -616,6 +594,64 @@ public:
return JSONRPCReplyObj(UniValue{result}, NullUniValue, 1);
}
const std::string m_help_doc{
"-netinfo level|\"help\" \n\n"
"Returns a network peer connections dashboard with information from the remote server.\n"
"This human-readable interface will change regularly and is not intended to be a stable API.\n"
"Under the hood, -netinfo fetches the data by calling getpeerinfo and getnetworkinfo.\n"
+ strprintf("An optional integer argument from 0 to %d can be passed for different peers listings; %d to 255 are parsed as %d.\n", MAX_DETAIL_LEVEL, MAX_DETAIL_LEVEL, MAX_DETAIL_LEVEL) +
"Pass \"help\" to see this detailed help documentation.\n"
"If more than one argument is passed, only the first one is read and parsed.\n"
"Suggestion: use with the Linux watch(1) command for a live dashboard; see example below.\n\n"
"Arguments:\n"
+ strprintf("1. level (integer 0-%d, optional) Specify the info level of the peers dashboard (default 0):\n", MAX_DETAIL_LEVEL) +
" 0 - Connection counts and local addresses\n"
" 1 - Like 0 but with a peers listing (without address or version columns)\n"
" 2 - Like 1 but with an address column\n"
" 3 - Like 1 but with a version column\n"
" 4 - Like 1 but with both address and version columns\n"
"2. help (string \"help\", optional) Print this help documentation instead of the dashboard.\n\n"
"Result:\n\n"
+ strprintf("* The peers listing in levels 1-%d displays all of the peers sorted by direction and minimum ping time:\n\n", MAX_DETAIL_LEVEL) +
" Column Description\n"
" ------ -----------\n"
" <-> Direction\n"
" \"in\" - inbound connections are those initiated by the peer\n"
" \"out\" - outbound connections are those initiated by us\n"
" type Type of peer connection\n"
" \"full\" - full relay, the default\n"
" \"block\" - block relay; like full relay but does not relay transactions or addresses\n"
" \"manual\" - peer we manually added using RPC addnode or the -addnode/-connect config options\n"
" \"feeler\" - short-lived connection for testing addresses\n"
" \"addr\" - address fetch; short-lived connection for requesting addresses\n"
" net Network the peer connected through (\"ipv4\", \"ipv6\", \"onion\", \"i2p\", or \"cjdns\")\n"
" mping Minimum observed ping time, in milliseconds (ms)\n"
" ping Last observed ping time, in milliseconds (ms)\n"
" send Time since last message sent to the peer, in seconds\n"
" recv Time since last message received from the peer, in seconds\n"
" txn Time since last novel transaction received from the peer and accepted into our mempool, in minutes\n"
" blk Time since last novel block passing initial validity checks received from the peer, in minutes\n"
" age Duration of connection to the peer, in minutes\n"
" asmap Mapped AS (Autonomous System) number in the BGP route to the peer, used for diversifying\n"
" peer selection (only displayed if the -asmap config option is set)\n"
" id Peer index, in increasing order of peer connections since node startup\n"
" address IP address and port of the peer\n"
" version Peer version and subversion concatenated, e.g. \"70016/Satoshi:21.0.0/\"\n\n"
"* The connection counts table displays the number of peers by direction, network, and the totals\n"
" for each, as well as two special outbound columns for block relay peers and manual peers.\n\n"
"* The local addresses table lists each local address broadcast by the node, the port, and the score.\n\n"
"Examples:\n\n"
"Connection counts and local addresses only\n"
"> dash-cli -netinfo\n\n"
"Compact peers listing\n"
"> dash-cli -netinfo 1\n\n"
"Full dashboard\n"
+ strprintf("> dash-cli -netinfo %d\n\n", MAX_DETAIL_LEVEL) +
"Full live dashboard, adjust --interval or --no-title as needed (Linux)\n"
+ strprintf("> watch --interval 1 --no-title dash-cli -netinfo %d\n\n", MAX_DETAIL_LEVEL) +
"See this help\n"
"> dash-cli -netinfo help\n"};
};
/** Process RPC generatetoaddress request. */
@ -866,6 +902,156 @@ static void GetWalletBalances(UniValue& result)
result.pushKV("balances", balances);
}
/**
* GetProgressBar contructs a progress bar with 5% intervals.
*
* @param[in] progress The proportion of the progress bar to be filled between 0 and 1.
* @param[out] progress_bar String representation of the progress bar.
*/
static void GetProgressBar(double progress, std::string& progress_bar)
{
if (progress < 0 || progress > 1) return;
static constexpr double INCREMENT{0.05};
static const std::string COMPLETE_BAR{"\u2592"};
static const std::string INCOMPLETE_BAR{"\u2591"};
for (int i = 0; i < progress / INCREMENT; ++i) {
progress_bar += COMPLETE_BAR;
}
for (int i = 0; i < (1 - progress) / INCREMENT; ++i) {
progress_bar += INCOMPLETE_BAR;
}
}
/**
* ParseGetInfoResult takes in -getinfo result in UniValue object and parses it
* into a user friendly UniValue string to be printed on the console.
* @param[out] result Reference to UniValue result containing the -getinfo output.
*/
static void ParseGetInfoResult(UniValue& result)
{
if (!find_value(result, "error").isNull()) return;
std::string RESET, GREEN, BLUE, YELLOW, MAGENTA, CYAN;
bool should_colorize = false;
#ifndef WIN32
if (isatty(fileno(stdout))) {
// By default, only print colored text if OS is not WIN32 and stdout is connected to a terminal.
should_colorize = true;
}
#endif
if (gArgs.IsArgSet("-color")) {
const std::string color{gArgs.GetArg("-color", DEFAULT_COLOR_SETTING)};
if (color == "always") {
should_colorize = true;
} else if (color == "never") {
should_colorize = false;
} else if (color != "auto") {
throw std::runtime_error("Invalid value for -color option. Valid values: always, auto, never.");
}
}
if (should_colorize) {
RESET = "\x1B[0m";
GREEN = "\x1B[32m";
BLUE = "\x1B[34m";
YELLOW = "\x1B[33m";
MAGENTA = "\x1B[35m";
CYAN = "\x1B[36m";
}
std::string result_string = strprintf("%sChain: %s%s\n", BLUE, result["chain"].getValStr(), RESET);
result_string += strprintf("Blocks: %s\n", result["blocks"].getValStr());
result_string += strprintf("Headers: %s\n", result["headers"].getValStr());
const double ibd_progress{result["verificationprogress"].get_real()};
std::string ibd_progress_bar;
// Display the progress bar only if IBD progress is less than 99%
if (ibd_progress < 0.99) {
GetProgressBar(ibd_progress, ibd_progress_bar);
// Add padding between progress bar and IBD progress
ibd_progress_bar += " ";
}
result_string += strprintf("Verification progress: %s%.4f%%\n", ibd_progress_bar, ibd_progress * 100);
result_string += strprintf("Difficulty: %s\n\n", result["difficulty"].getValStr());
result_string += strprintf(
"%sNetwork: in %s, out %s, total %s, mn_in %s, mn_out %s, mn_total %s%s\n",
GREEN,
result["connections"]["in"].getValStr(),
result["connections"]["out"].getValStr(),
result["connections"]["total"].getValStr(),
result["connections"]["mn_in"].getValStr(),
result["connections"]["mn_out"].getValStr(),
result["connections"]["mn_total"].getValStr(),
RESET);
result_string += strprintf("Version: %s\n", result["version"].getValStr());
result_string += strprintf("Time offset (s): %s\n", result["timeoffset"].getValStr());
// proxies
std::map<std::string, std::vector<std::string>> proxy_networks;
std::vector<std::string> ordered_proxies;
for (const UniValue& network : result["networks"].getValues()) {
const std::string proxy = network["proxy"].getValStr();
if (proxy.empty()) continue;
// Add proxy to ordered_proxy if has not been processed
if (proxy_networks.find(proxy) == proxy_networks.end()) ordered_proxies.push_back(proxy);
proxy_networks[proxy].push_back(network["name"].getValStr());
}
std::vector<std::string> formatted_proxies;
for (const std::string& proxy : ordered_proxies) {
formatted_proxies.emplace_back(strprintf("%s (%s)", proxy, Join(proxy_networks.find(proxy)->second, ", ")));
}
result_string += strprintf("Proxies: %s\n", formatted_proxies.empty() ? "n/a" : Join(formatted_proxies, ", "));
result_string += strprintf("Min tx relay fee rate (%s/kB): %s\n\n", CURRENCY_UNIT, result["relayfee"].getValStr());
if (!result["has_wallet"].isNull()) {
const std::string walletname = result["walletname"].getValStr();
result_string += strprintf("%sWallet: %s%s\n", MAGENTA, walletname.empty() ? "\"\"" : walletname, RESET);
result_string += strprintf("%sCoinJoin balance:%s %s\n", CYAN, RESET, result["coinjoin_balance"].getValStr());
result_string += strprintf("Keypool size: %s\n", result["keypoolsize"].getValStr());
if (!result["unlocked_until"].isNull()) {
result_string += strprintf("Unlocked until: %s\n", result["unlocked_until"].getValStr());
}
result_string += strprintf("Transaction fee rate (-paytxfee) (%s/kB): %s\n\n", CURRENCY_UNIT, result["paytxfee"].getValStr());
}
if (!result["balance"].isNull()) {
result_string += strprintf("%sBalance:%s %s\n\n", CYAN, RESET, result["balance"].getValStr());
}
if (!result["balances"].isNull()) {
result_string += strprintf("%sBalances%s\n", CYAN, RESET);
size_t max_balance_length{10};
for (const std::string& wallet : result["balances"].getKeys()) {
max_balance_length = std::max(result["balances"][wallet].getValStr().length(), max_balance_length);
}
for (const std::string& wallet : result["balances"].getKeys()) {
result_string += strprintf("%*s %s\n",
max_balance_length,
result["balances"][wallet].getValStr(),
wallet.empty() ? "\"\"" : wallet);
}
result_string += "\n";
}
result_string += strprintf("%sWarnings:%s %s", YELLOW, RESET, result["warnings"].getValStr());
result.setStr(result_string);
}
/**
* Call RPC getnewaddress.
* @returns getnewaddress response as a UniValue object.
@ -953,6 +1139,10 @@ static int CommandLineRPC(int argc, char *argv[])
if (gArgs.IsArgSet("-getinfo")) {
rh.reset(new GetinfoRequestHandler());
} else if (gArgs.GetBoolArg("-netinfo", false)) {
if (!args.empty() && args.at(0) == "help") {
tfm::format(std::cout, "%s\n", NetinfoRequestHandler().m_help_doc);
return 0;
}
rh.reset(new NetinfoRequestHandler());
} else if (gArgs.GetBoolArg("-generate", false)) {
const UniValue getnewaddress{GetNewAddress()};
@ -983,9 +1173,13 @@ static int CommandLineRPC(int argc, char *argv[])
UniValue result = find_value(reply, "result");
const UniValue& error = find_value(reply, "error");
if (error.isNull()) {
if (gArgs.IsArgSet("-getinfo") && !gArgs.IsArgSet("-rpcwallet")) {
GetWalletBalances(result); // fetch multiwallet balances and append to result
if (gArgs.GetBoolArg("-getinfo", false)) {
if (!gArgs.IsArgSet("-rpcwallet")) {
GetWalletBalances(result); // fetch multiwallet balances and append to result
}
ParseGetInfoResult(result);
}
ParseResult(result, strPrint);
} else {
ParseError(error, strPrint, nRet);

View File

@ -62,6 +62,7 @@
#endif
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include <unordered_map>
@ -913,18 +914,6 @@ static bool ReverseCompareNodeTimeConnected(const NodeEvictionCandidate& a, cons
return a.nTimeConnected > b.nTimeConnected;
}
static bool CompareLocalHostTimeConnected(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b)
{
if (a.m_is_local != b.m_is_local) return b.m_is_local;
return a.nTimeConnected > b.nTimeConnected;
}
static bool CompareOnionTimeConnected(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b)
{
if (a.m_is_onion != b.m_is_onion) return b.m_is_onion;
return a.nTimeConnected > b.nTimeConnected;
}
static bool CompareNetGroupKeyed(const NodeEvictionCandidate &a, const NodeEvictionCandidate &b) {
return a.nKeyedNetGroup < b.nKeyedNetGroup;
}
@ -955,6 +944,26 @@ static bool CompareNodeBlockRelayOnlyTime(const NodeEvictionCandidate &a, const
return a.nTimeConnected > b.nTimeConnected;
}
/**
* Sort eviction candidates by network/localhost and connection uptime.
* Candidates near the beginning are more likely to be evicted, and those
* near the end are more likely to be protected, e.g. less likely to be evicted.
* - First, nodes that are not `is_local` and that do not belong to `network`,
* sorted by increasing uptime (from most recently connected to connected longer).
* - Then, nodes that are `is_local` or belong to `network`, sorted by increasing uptime.
*/
struct CompareNodeNetworkTime {
const bool m_is_local;
const Network m_network;
CompareNodeNetworkTime(bool is_local, Network network) : m_is_local(is_local), m_network(network) {}
bool operator()(const NodeEvictionCandidate& a, const NodeEvictionCandidate& b) const
{
if (m_is_local && a.m_is_local != b.m_is_local) return b.m_is_local;
if ((a.m_network == m_network) != (b.m_network == m_network)) return b.m_network == m_network;
return a.nTimeConnected > b.nTimeConnected;
};
};
//! Sort an array by the specified comparator, then erase the last K elements where predicate is true.
template <typename T, typename Comparator>
static void EraseLastKElements(
@ -966,40 +975,72 @@ static void EraseLastKElements(
elements.erase(std::remove_if(elements.end() - eraseSize, elements.end(), predicate), elements.end());
}
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates)
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& eviction_candidates)
{
// Protect the half of the remaining nodes which have been connected the longest.
// This replicates the non-eviction implicit behavior, and precludes attacks that start later.
// To favorise the diversity of our peer connections, reserve up to (half + 2) of
// these protected spots for onion and localhost peers, if any, even if they're not
// longest uptime overall. This helps protect tor peers, which tend to be otherwise
// To favorise the diversity of our peer connections, reserve up to half of these protected
// spots for Tor/onion, localhost and I2P peers, even if they're not longest uptime overall.
// This helps protect these higher-latency peers that tend to be otherwise
// disadvantaged under our eviction criteria.
const size_t initial_size = vEvictionCandidates.size();
size_t total_protect_size = initial_size / 2;
const size_t onion_protect_size = total_protect_size / 2;
const size_t initial_size = eviction_candidates.size();
const size_t total_protect_size{initial_size / 2};
if (onion_protect_size) {
// Pick out up to 1/4 peers connected via our onion service, sorted by longest uptime.
EraseLastKElements(vEvictionCandidates, CompareOnionTimeConnected, onion_protect_size,
[](const NodeEvictionCandidate& n) { return n.m_is_onion; });
// Disadvantaged networks to protect: I2P, localhost, Tor/onion. In case of equal counts, earlier
// array members have first opportunity to recover unused slots from the previous iteration.
struct Net { bool is_local; Network id; size_t count; };
std::array<Net, 3> networks{
{{false, NET_I2P, 0}, {/* localhost */ true, NET_MAX, 0}, {false, NET_ONION, 0}}};
// Count and store the number of eviction candidates per network.
for (Net& n : networks) {
n.count = std::count_if(eviction_candidates.cbegin(), eviction_candidates.cend(),
[&n](const NodeEvictionCandidate& c) {
return n.is_local ? c.m_is_local : c.m_network == n.id;
});
}
// Sort `networks` by ascending candidate count, to give networks having fewer candidates
// the first opportunity to recover unused protected slots from the previous iteration.
std::stable_sort(networks.begin(), networks.end(), [](Net a, Net b) { return a.count < b.count; });
const size_t localhost_min_protect_size{2};
if (onion_protect_size >= localhost_min_protect_size) {
// Allocate any remaining slots of the 1/4, or minimum 2 additional slots,
// to localhost peers, sorted by longest uptime, as manually configured
// hidden services not using `-bind=addr[:port]=onion` will not be detected
// as inbound onion connections.
const size_t remaining_tor_slots{onion_protect_size - (initial_size - vEvictionCandidates.size())};
const size_t localhost_protect_size{std::max(remaining_tor_slots, localhost_min_protect_size)};
EraseLastKElements(vEvictionCandidates, CompareLocalHostTimeConnected, localhost_protect_size,
[](const NodeEvictionCandidate& n) { return n.m_is_local; });
// Protect up to 25% of the eviction candidates by disadvantaged network.
const size_t max_protect_by_network{total_protect_size / 2};
size_t num_protected{0};
while (num_protected < max_protect_by_network) {
const size_t disadvantaged_to_protect{max_protect_by_network - num_protected};
const size_t protect_per_network{
std::max(disadvantaged_to_protect / networks.size(), static_cast<size_t>(1))};
// Early exit flag if there are no remaining candidates by disadvantaged network.
bool protected_at_least_one{false};
for (const Net& n : networks) {
if (n.count == 0) continue;
const size_t before = eviction_candidates.size();
EraseLastKElements(eviction_candidates, CompareNodeNetworkTime(n.is_local, n.id),
protect_per_network, [&n](const NodeEvictionCandidate& c) {
return n.is_local ? c.m_is_local : c.m_network == n.id;
});
const size_t after = eviction_candidates.size();
if (before > after) {
protected_at_least_one = true;
num_protected += before - after;
if (num_protected >= max_protect_by_network) {
break;
}
}
}
if (!protected_at_least_one) {
break;
}
}
// Calculate how many we removed, and update our total number of peers that
// we want to protect based on uptime accordingly.
total_protect_size -= initial_size - vEvictionCandidates.size();
EraseLastKElements(vEvictionCandidates, ReverseCompareNodeTimeConnected, total_protect_size);
assert(num_protected == initial_size - eviction_candidates.size());
const size_t remaining_to_protect{total_protect_size - num_protected};
EraseLastKElements(eviction_candidates, ReverseCompareNodeTimeConnected, remaining_to_protect);
}
[[nodiscard]] std::optional<NodeId> SelectNodeToEvict(std::vector<NodeEvictionCandidate>&& vEvictionCandidates)
@ -1016,8 +1057,7 @@ void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvict
// An attacker cannot manipulate this metric without performing useful work.
EraseLastKElements(vEvictionCandidates, CompareNodeTXTime, 4);
// Protect up to 8 non-tx-relay peers that have sent us novel blocks.
const size_t erase_size = std::min(size_t(8), vEvictionCandidates.size());
EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, erase_size,
EraseLastKElements(vEvictionCandidates, CompareNodeBlockRelayOnlyTime, 8,
[](const NodeEvictionCandidate& n) { return !n.m_relay_txs && n.fRelevantServices; });
// Protect 4 nodes that most recently sent us novel blocks.
@ -1109,7 +1149,7 @@ bool CConnman::AttemptToEvictConnection()
HasAllDesirableServiceFlags(node->nServices),
node->m_relays_txs.load(), node->m_bloom_filter_loaded.load(),
node->nKeyedNetGroup, node->m_prefer_evict, node->addr.IsLocal(),
node->m_inbound_onion};
node->ConnectedThroughNetwork()};
vEvictionCandidates.push_back(candidate);
}
}

View File

@ -1583,7 +1583,7 @@ struct NodeEvictionCandidate
uint64_t nKeyedNetGroup;
bool prefer_evict;
bool m_is_local;
bool m_is_onion;
Network m_network;
};
/**
@ -1613,20 +1613,20 @@ size_t GetRequestedObjectCount(NodeId nodeId) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
* longest, to replicate the non-eviction implicit behavior and preclude attacks
* that start later.
*
* Half of these protected spots (1/4 of the total) are reserved for onion peers
* connected via our tor control service, if any, sorted by longest uptime, even
* if they're not longest uptime overall. Any remaining slots of the 1/4 are
* then allocated to protect localhost peers, if any (or up to 2 localhost peers
* if no slots remain and 2 or more onion peers were protected), sorted by
* longest uptime, as manually configured hidden services not using
* `-bind=addr[:port]=onion` will not be detected as inbound onion connections.
* Half of these protected spots (1/4 of the total) are reserved for the
* following categories of peers, sorted by longest uptime, even if they're not
* longest uptime overall:
*
* This helps protect onion peers, which tend to be otherwise disadvantaged
* under our eviction criteria for their higher min ping times relative to IPv4
* and IPv6 peers, and favorise the diversity of peer connections.
* - onion peers connected via our tor control service
*
* This function was extracted from SelectNodeToEvict() to be able to test the
* ratio-based protection logic deterministically.
* - localhost peers, as manually configured hidden services not using
* `-bind=addr[:port]=onion` will not be detected as inbound onion connections
*
* - I2P peers
*
* This helps protect these privacy network peers, which tend to be otherwise
* disadvantaged under our eviction criteria for their higher min ping times
* relative to IPv4/IPv6 peers, and favorise the diversity of peer connections.
*/
void ProtectEvictionCandidatesByRatio(std::vector<NodeEvictionCandidate>& vEvictionCandidates);

View File

@ -31,7 +31,7 @@ FUZZ_TARGET(node_eviction)
/* nKeyedNetGroup */ fuzzed_data_provider.ConsumeIntegral<uint64_t>(),
/* prefer_evict */ fuzzed_data_provider.ConsumeBool(),
/* m_is_local */ fuzzed_data_provider.ConsumeBool(),
/* m_is_onion */ fuzzed_data_provider.ConsumeBool(),
/* m_network */ fuzzed_data_provider.PickValueInArray(ALL_NETWORKS),
});
}
// Make a copy since eviction_candidates may be in some valid but otherwise

View File

@ -2,7 +2,9 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <netaddress.h>
#include <net.h>
#include <test/util/net.h>
#include <test/util/setup_common.h>
#include <boost/test/unit_test.hpp>
@ -15,11 +17,6 @@
BOOST_FIXTURE_TEST_SUITE(net_peer_eviction_tests, BasicTestingSetup)
namespace {
constexpr int NODE_EVICTION_TEST_ROUNDS{10};
constexpr int NODE_EVICTION_TEST_UP_TO_N_NODES{200};
} // namespace
std::vector<NodeEvictionCandidate> GetRandomNodeEvictionCandidates(const int n_candidates, FastRandomContext& random_context)
{
std::vector<NodeEvictionCandidate> candidates;
@ -36,7 +33,7 @@ std::vector<NodeEvictionCandidate> GetRandomNodeEvictionCandidates(const int n_c
/* nKeyedNetGroup */ random_context.randrange(100),
/* prefer_evict */ random_context.randbool(),
/* m_is_local */ random_context.randbool(),
/* m_is_onion */ random_context.randbool(),
/* m_network */ ALL_NETWORKS[random_context.randrange(ALL_NETWORKS.size())],
});
}
return candidates;
@ -94,7 +91,8 @@ BOOST_AUTO_TEST_CASE(peer_protection_test)
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = c.m_is_local = false;
c.m_is_local = false;
c.m_network = NET_IPV4;
},
/* protected_peer_ids */ {0, 1, 2, 3, 4, 5},
/* unprotected_peer_ids */ {6, 7, 8, 9, 10, 11},
@ -104,129 +102,359 @@ BOOST_AUTO_TEST_CASE(peer_protection_test)
BOOST_CHECK(IsProtected(
num_peers, [num_peers](NodeEvictionCandidate& c) {
c.nTimeConnected = num_peers - c.id;
c.m_is_onion = c.m_is_local = false;
c.m_is_local = false;
c.m_network = NET_IPV6;
},
/* protected_peer_ids */ {6, 7, 8, 9, 10, 11},
/* unprotected_peer_ids */ {0, 1, 2, 3, 4, 5},
random_context));
// Test protection of onion and localhost peers...
// Test protection of onion, localhost, and I2P peers...
// Expect 1/4 onion peers to be protected from eviction,
// independently of other characteristics.
// if no localhost or I2P peers.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.m_is_onion = (c.id == 3 || c.id == 8 || c.id == 9);
c.m_is_local = false;
c.m_network = (c.id == 3 || c.id == 8 || c.id == 9) ? NET_ONION : NET_IPV4;
},
/* protected_peer_ids */ {3, 8, 9},
/* unprotected_peer_ids */ {},
random_context));
// Expect 1/4 onion peers and 1/4 of the others to be protected
// from eviction, sorted by longest uptime (lowest nTimeConnected).
// Expect 1/4 onion peers and 1/4 of the other peers to be protected,
// sorted by longest uptime (lowest nTimeConnected), if no localhost or I2P peers.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = false;
c.m_is_onion = (c.id == 3 || c.id > 7);
c.m_network = (c.id == 3 || c.id > 7) ? NET_ONION : NET_IPV6;
},
/* protected_peer_ids */ {0, 1, 2, 3, 8, 9},
/* unprotected_peer_ids */ {4, 5, 6, 7, 10, 11},
random_context));
// Expect 1/4 localhost peers to be protected from eviction,
// if no onion peers.
// if no onion or I2P peers.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.m_is_onion = false;
c.m_is_local = (c.id == 1 || c.id == 9 || c.id == 11);
c.m_network = NET_IPV4;
},
/* protected_peer_ids */ {1, 9, 11},
/* unprotected_peer_ids */ {},
random_context));
// Expect 1/4 localhost peers and 1/4 of the other peers to be protected,
// sorted by longest uptime (lowest nTimeConnected), if no onion peers.
// sorted by longest uptime (lowest nTimeConnected), if no onion or I2P peers.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = false;
c.m_is_local = (c.id > 6);
c.m_network = NET_IPV6;
},
/* protected_peer_ids */ {0, 1, 2, 7, 8, 9},
/* unprotected_peer_ids */ {3, 4, 5, 6, 10, 11},
random_context));
// Combined test: expect 1/4 onion and 2 localhost peers to be protected
// from eviction, sorted by longest uptime.
// Expect 1/4 I2P peers to be protected from eviction,
// if no onion or localhost peers.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.m_is_local = false;
c.m_network = (c.id == 2 || c.id == 7 || c.id == 10) ? NET_I2P : NET_IPV4;
},
/* protected_peer_ids */ {2, 7, 10},
/* unprotected_peer_ids */ {},
random_context));
// Expect 1/4 I2P peers and 1/4 of the other peers to be protected,
// sorted by longest uptime (lowest nTimeConnected), if no onion or localhost peers.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = (c.id == 0 || c.id == 5 || c.id == 10);
c.m_is_local = (c.id == 1 || c.id == 9 || c.id == 11);
c.m_is_local = false;
c.m_network = (c.id == 4 || c.id > 8) ? NET_I2P : NET_IPV6;
},
/* protected_peer_ids */ {0, 1, 2, 5, 9, 10},
/* unprotected_peer_ids */ {3, 4, 6, 7, 8, 11},
/* protected_peer_ids */ {0, 1, 2, 4, 9, 10},
/* unprotected_peer_ids */ {3, 5, 6, 7, 8, 11},
random_context));
// Combined test: expect having only 1 onion to allow allocating the
// remaining 2 of the 1/4 to localhost peers, sorted by longest uptime.
// Tests with 2 networks...
// Combined test: expect having 1 localhost and 1 onion peer out of 4 to
// protect 1 localhost, 0 onion and 1 other peer, sorted by longest uptime;
// stable sort breaks tie with array order of localhost first.
BOOST_CHECK(IsProtected(
num_peers + 4, [](NodeEvictionCandidate& c) {
4, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = (c.id == 15);
c.m_is_local = (c.id > 6 && c.id < 11);
c.m_is_local = (c.id == 4);
c.m_network = (c.id == 3) ? NET_ONION : NET_IPV4;
},
/* protected_peer_ids */ {0, 1, 2, 3, 7, 8, 9, 15},
/* unprotected_peer_ids */ {4, 5, 6, 10, 11, 12, 13, 14},
/* protected_peer_ids */ {0, 4},
/* unprotected_peer_ids */ {1, 2},
random_context));
// Combined test: expect 2 onions (< 1/4) to allow allocating the minimum 2
// localhost peers, sorted by longest uptime.
// Combined test: expect having 1 localhost and 1 onion peer out of 7 to
// protect 1 localhost, 0 onion, and 2 other peers (3 total), sorted by
// uptime; stable sort breaks tie with array order of localhost first.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
7, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = (c.id == 7 || c.id == 9);
c.m_is_local = (c.id == 6 || c.id == 11);
c.m_is_local = (c.id == 6);
c.m_network = (c.id == 5) ? NET_ONION : NET_IPV4;
},
/* protected_peer_ids */ {0, 1, 6, 7, 9, 11},
/* unprotected_peer_ids */ {2, 3, 4, 5, 8, 10},
/* protected_peer_ids */ {0, 1, 6},
/* unprotected_peer_ids */ {2, 3, 4, 5},
random_context));
// Combined test: when > 1/4, expect max 1/4 onion and 2 localhost peers
// to be protected from eviction, sorted by longest uptime.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = (c.id > 3 && c.id < 8);
c.m_is_local = (c.id > 7);
},
/* protected_peer_ids */ {0, 4, 5, 6, 8, 9},
/* unprotected_peer_ids */ {1, 2, 3, 7, 10, 11},
random_context));
// Combined test: idem > 1/4 with only 8 peers: expect 2 onion and 2
// localhost peers (1/4 + 2) to be protected, sorted by longest uptime.
// Combined test: expect having 1 localhost and 1 onion peer out of 8 to
// protect protect 1 localhost, 1 onion and 2 other peers (4 total), sorted
// by uptime; stable sort breaks tie with array order of localhost first.
BOOST_CHECK(IsProtected(
8, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = (c.id > 1 && c.id < 5);
c.m_is_local = (c.id > 4);
c.m_is_local = (c.id == 6);
c.m_network = (c.id == 5) ? NET_ONION : NET_IPV4;
},
/* protected_peer_ids */ {2, 3, 5, 6},
/* unprotected_peer_ids */ {0, 1, 4, 7},
/* protected_peer_ids */ {0, 1, 5, 6},
/* unprotected_peer_ids */ {2, 3, 4, 7},
random_context));
// Combined test: idem > 1/4 with only 6 peers: expect 1 onion peer and no
// localhost peers (1/4 + 0) to be protected, sorted by longest uptime.
// Combined test: expect having 3 localhost and 3 onion peers out of 12 to
// protect 2 localhost and 1 onion, plus 3 other peers, sorted by longest
// uptime; stable sort breaks ties with the array order of localhost first.
BOOST_CHECK(IsProtected(
6, [](NodeEvictionCandidate& c) {
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_onion = (c.id == 4 || c.id == 5);
c.m_is_local = (c.id == 3);
c.m_is_local = (c.id == 6 || c.id == 9 || c.id == 11);
c.m_network = (c.id == 7 || c.id == 8 || c.id == 10) ? NET_ONION : NET_IPV6;
},
/* protected_peer_ids */ {0, 1, 4},
/* unprotected_peer_ids */ {2, 3, 5},
/* protected_peer_ids */ {0, 1, 2, 6, 7, 9},
/* unprotected_peer_ids */ {3, 4, 5, 8, 10, 11},
random_context));
// Combined test: expect having 4 localhost and 1 onion peer out of 12 to
// protect 2 localhost and 1 onion, plus 3 other peers, sorted by longest uptime.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id > 4 && c.id < 9);
c.m_network = (c.id == 10) ? NET_ONION : NET_IPV4;
},
/* protected_peer_ids */ {0, 1, 2, 5, 6, 10},
/* unprotected_peer_ids */ {3, 4, 7, 8, 9, 11},
random_context));
// Combined test: expect having 4 localhost and 2 onion peers out of 16 to
// protect 2 localhost and 2 onions, plus 4 other peers, sorted by longest uptime.
BOOST_CHECK(IsProtected(
16, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 6 || c.id == 9 || c.id == 11 || c.id == 12);
c.m_network = (c.id == 8 || c.id == 10) ? NET_ONION : NET_IPV6;
},
/* protected_peer_ids */ {0, 1, 2, 3, 6, 8, 9, 10},
/* unprotected_peer_ids */ {4, 5, 7, 11, 12, 13, 14, 15},
random_context));
// Combined test: expect having 5 localhost and 1 onion peer out of 16 to
// protect 3 localhost (recovering the unused onion slot), 1 onion, and 4
// others, sorted by longest uptime.
BOOST_CHECK(IsProtected(
16, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id > 10);
c.m_network = (c.id == 10) ? NET_ONION : NET_IPV4;
},
/* protected_peer_ids */ {0, 1, 2, 3, 10, 11, 12, 13},
/* unprotected_peer_ids */ {4, 5, 6, 7, 8, 9, 14, 15},
random_context));
// Combined test: expect having 1 localhost and 4 onion peers out of 16 to
// protect 1 localhost and 3 onions (recovering the unused localhost slot),
// plus 4 others, sorted by longest uptime.
BOOST_CHECK(IsProtected(
16, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 15);
c.m_network = (c.id > 6 && c.id < 11) ? NET_ONION : NET_IPV6;
},
/* protected_peer_ids */ {0, 1, 2, 3, 7, 8, 9, 15},
/* unprotected_peer_ids */ {5, 6, 10, 11, 12, 13, 14},
random_context));
// Combined test: expect having 2 onion and 4 I2P out of 12 peers to protect
// 2 onion (prioritized for having fewer candidates) and 1 I2P, plus 3
// others, sorted by longest uptime.
BOOST_CHECK(IsProtected(
num_peers, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = false;
if (c.id == 8 || c.id == 10) {
c.m_network = NET_ONION;
} else if (c.id == 6 || c.id == 9 || c.id == 11 || c.id == 12) {
c.m_network = NET_I2P;
} else {
c.m_network = NET_IPV4;
}
},
/* protected_peer_ids */ {0, 1, 2, 6, 8, 10},
/* unprotected_peer_ids */ {3, 4, 5, 7, 9, 11},
random_context));
// Tests with 3 networks...
// Combined test: expect having 1 localhost, 1 I2P and 1 onion peer out of 4
// to protect 1 I2P, 0 localhost, 0 onion and 1 other peer (2 total), sorted
// by longest uptime; stable sort breaks tie with array order of I2P first.
BOOST_CHECK(IsProtected(
4, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 3);
if (c.id == 4) {
c.m_network = NET_I2P;
} else if (c.id == 2) {
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV6;
}
},
/* protected_peer_ids */ {0, 4},
/* unprotected_peer_ids */ {1, 2},
random_context));
// Combined test: expect having 1 localhost, 1 I2P and 1 onion peer out of 7
// to protect 1 I2P, 0 localhost, 0 onion and 2 other peers (3 total) sorted
// by longest uptime; stable sort breaks tie with array order of I2P first.
BOOST_CHECK(IsProtected(
7, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 4);
if (c.id == 6) {
c.m_network = NET_I2P;
} else if (c.id == 5) {
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV6;
}
},
/* protected_peer_ids */ {0, 1, 6},
/* unprotected_peer_ids */ {2, 3, 4, 5},
random_context));
// Combined test: expect having 1 localhost, 1 I2P and 1 onion peer out of 8
// to protect 1 I2P, 1 localhost, 0 onion and 2 other peers (4 total) sorted
// by uptime; stable sort breaks tie with array order of I2P then localhost.
BOOST_CHECK(IsProtected(
8, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 6);
if (c.id == 5) {
c.m_network = NET_I2P;
} else if (c.id == 4) {
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV6;
}
},
/* protected_peer_ids */ {0, 1, 5, 6},
/* unprotected_peer_ids */ {2, 3, 4, 7},
random_context));
// Combined test: expect having 4 localhost, 2 I2P, and 2 onion peers out of
// 16 to protect 1 localhost, 2 I2P, and 1 onion (4/16 total), plus 4 others
// for 8 total, sorted by longest uptime.
BOOST_CHECK(IsProtected(
16, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 6 || c.id > 11);
if (c.id == 7 || c.id == 11) {
c.m_network = NET_I2P;
} else if (c.id == 9 || c.id == 10) {
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV4;
}
},
/* protected_peer_ids */ {0, 1, 2, 3, 6, 7, 9, 11},
/* unprotected_peer_ids */ {4, 5, 8, 10, 12, 13, 14, 15},
random_context));
// Combined test: expect having 1 localhost, 8 I2P and 1 onion peer out of
// 24 to protect 1, 4, and 1 (6 total), plus 6 others for 12/24 total,
// sorted by longest uptime.
BOOST_CHECK(IsProtected(
24, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 12);
if (c.id > 14 && c.id < 23) { // 4 protected instead of usual 2
c.m_network = NET_I2P;
} else if (c.id == 23) {
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV6;
}
},
/* protected_peer_ids */ {0, 1, 2, 3, 4, 5, 12, 15, 16, 17, 18, 23},
/* unprotected_peer_ids */ {6, 7, 8, 9, 10, 11, 13, 14, 19, 20, 21, 22},
random_context));
// Combined test: expect having 1 localhost, 3 I2P and 6 onion peers out of
// 24 to protect 1, 3, and 2 (6 total, I2P has fewer candidates and so gets the
// unused localhost slot), plus 6 others for 12/24 total, sorted by longest uptime.
BOOST_CHECK(IsProtected(
24, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 15);
if (c.id == 12 || c.id == 14 || c.id == 17) {
c.m_network = NET_I2P;
} else if (c.id > 17) { // 4 protected instead of usual 2
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV4;
}
},
/* protected_peer_ids */ {0, 1, 2, 3, 4, 5, 12, 14, 15, 17, 18, 19},
/* unprotected_peer_ids */ {6, 7, 8, 9, 10, 11, 13, 16, 20, 21, 22, 23},
random_context));
// Combined test: expect having 1 localhost, 7 I2P and 4 onion peers out of
// 24 to protect 1 localhost, 2 I2P, and 3 onions (6 total), plus 6 others
// for 12/24 total, sorted by longest uptime.
BOOST_CHECK(IsProtected(
24, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id == 13);
if (c.id > 16) {
c.m_network = NET_I2P;
} else if (c.id == 12 || c.id == 14 || c.id == 15 || c.id == 16) {
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV6;
}
},
/* protected_peer_ids */ {0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 17, 18},
/* unprotected_peer_ids */ {6, 7, 8, 9, 10, 11, 16, 19, 20, 21, 22, 23},
random_context));
// Combined test: expect having 8 localhost, 4 I2P, and 3 onion peers out of
// 24 to protect 2 of each (6 total), plus 6 others for 12/24 total, sorted
// by longest uptime.
BOOST_CHECK(IsProtected(
24, [](NodeEvictionCandidate& c) {
c.nTimeConnected = c.id;
c.m_is_local = (c.id > 15);
if (c.id > 10 && c.id < 15) {
c.m_network = NET_I2P;
} else if (c.id > 6 && c.id < 10) {
c.m_network = NET_ONION;
} else {
c.m_network = NET_IPV4;
}
},
/* protected_peer_ids */ {0, 1, 2, 3, 4, 5, 7, 8, 11, 12, 16, 17},
/* unprotected_peer_ids */ {6, 9, 10, 13, 14, 15, 18, 19, 20, 21, 22, 23},
random_context));
}
@ -257,91 +485,89 @@ BOOST_AUTO_TEST_CASE(peer_eviction_test)
{
FastRandomContext random_context{true};
for (int i = 0; i < NODE_EVICTION_TEST_ROUNDS; ++i) {
for (int number_of_nodes = 0; number_of_nodes < NODE_EVICTION_TEST_UP_TO_N_NODES; ++number_of_nodes) {
// Four nodes with the highest keyed netgroup values should be
// protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nKeyedNetGroup = number_of_nodes - candidate.id;
},
{0, 1, 2, 3}, random_context));
for (int number_of_nodes = 0; number_of_nodes < 200; ++number_of_nodes) {
// Four nodes with the highest keyed netgroup values should be
// protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nKeyedNetGroup = number_of_nodes - candidate.id;
},
{0, 1, 2, 3}, random_context));
// Eight nodes with the lowest minimum ping time should be protected
// from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [](NodeEvictionCandidate& candidate) {
candidate.m_min_ping_time = std::chrono::microseconds{candidate.id};
},
{0, 1, 2, 3, 4, 5, 6, 7}, random_context));
// Eight nodes with the lowest minimum ping time should be protected
// from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [](NodeEvictionCandidate& candidate) {
candidate.m_min_ping_time = std::chrono::microseconds{candidate.id};
},
{0, 1, 2, 3, 4, 5, 6, 7}, random_context));
// Four nodes that most recently sent us novel transactions accepted
// into our mempool should be protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastTXTime = number_of_nodes - candidate.id;
},
{0, 1, 2, 3}, random_context));
// Four nodes that most recently sent us novel transactions accepted
// into our mempool should be protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastTXTime = number_of_nodes - candidate.id;
},
{0, 1, 2, 3}, random_context));
// Up to eight non-tx-relay peers that most recently sent us novel
// blocks should be protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastBlockTime = number_of_nodes - candidate.id;
if (candidate.id <= 7) {
candidate.m_relay_txs = false;
candidate.fRelevantServices = true;
}
},
{0, 1, 2, 3, 4, 5, 6, 7}, random_context));
// Up to eight non-tx-relay peers that most recently sent us novel
// blocks should be protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastBlockTime = number_of_nodes - candidate.id;
if (candidate.id <= 7) {
candidate.m_relay_txs = false;
candidate.fRelevantServices = true;
}
},
{0, 1, 2, 3, 4, 5, 6, 7}, random_context));
// Four peers that most recently sent us novel blocks should be
// protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastBlockTime = number_of_nodes - candidate.id;
},
{0, 1, 2, 3}, random_context));
// Four peers that most recently sent us novel blocks should be
// protected from eviction.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastBlockTime = number_of_nodes - candidate.id;
},
{0, 1, 2, 3}, random_context));
// Combination of the previous two tests.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastBlockTime = number_of_nodes - candidate.id;
if (candidate.id <= 7) {
candidate.m_relay_txs = false;
candidate.fRelevantServices = true;
}
},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, random_context));
// Combination of the previous two tests.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nLastBlockTime = number_of_nodes - candidate.id;
if (candidate.id <= 7) {
candidate.m_relay_txs = false;
candidate.fRelevantServices = true;
}
},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, random_context));
// Combination of all tests above.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nKeyedNetGroup = number_of_nodes - candidate.id; // 4 protected
candidate.m_min_ping_time = std::chrono::microseconds{candidate.id}; // 8 protected
candidate.nLastTXTime = number_of_nodes - candidate.id; // 4 protected
candidate.nLastBlockTime = number_of_nodes - candidate.id; // 4 protected
},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, random_context));
// Combination of all tests above.
BOOST_CHECK(!IsEvicted(
number_of_nodes, [number_of_nodes](NodeEvictionCandidate& candidate) {
candidate.nKeyedNetGroup = number_of_nodes - candidate.id; // 4 protected
candidate.m_min_ping_time = std::chrono::microseconds{candidate.id}; // 8 protected
candidate.nLastTXTime = number_of_nodes - candidate.id; // 4 protected
candidate.nLastBlockTime = number_of_nodes - candidate.id; // 4 protected
},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, random_context));
// An eviction is expected given >= 29 random eviction candidates. The eviction logic protects at most
// four peers by net group, eight by lowest ping time, four by last time of novel tx, up to eight non-tx-relay
// peers by last novel block time, and four more peers by last novel block time.
if (number_of_nodes >= 29) {
BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
}
// No eviction is expected given <= 20 random eviction candidates. The eviction logic protects at least
// four peers by net group, eight by lowest ping time, four by last time of novel tx and four peers by last
// novel block time.
if (number_of_nodes <= 20) {
BOOST_CHECK(!SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
}
// Cases left to test:
// * "If any remaining peers are preferred for eviction consider only them. [...]"
// * "Identify the network group with the most connections and youngest member. [...]"
// An eviction is expected given >= 29 random eviction candidates. The eviction logic protects at most
// four peers by net group, eight by lowest ping time, four by last time of novel tx, up to eight non-tx-relay
// peers by last novel block time, and four more peers by last novel block time.
if (number_of_nodes >= 29) {
BOOST_CHECK(SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
}
// No eviction is expected given <= 20 random eviction candidates. The eviction logic protects at least
// four peers by net group, eight by lowest ping time, four by last time of novel tx and four peers by last
// novel block time.
if (number_of_nodes <= 20) {
BOOST_CHECK(!SelectNodeToEvict(GetRandomNodeEvictionCandidates(number_of_nodes, random_context)));
}
// Cases left to test:
// * "If any remaining peers are preferred for eviction consider only them. [...]"
// * "Identify the network group with the most connections and youngest member. [...]"
}
}

View File

@ -6,9 +6,11 @@
#define BITCOIN_TEST_UTIL_NET_H
#include <compat.h>
#include <netaddress.h>
#include <net.h>
#include <util/sock.h>
#include <array>
#include <cassert>
#include <cstring>
#include <string>
@ -73,6 +75,16 @@ constexpr ConnectionType ALL_CONNECTION_TYPES[]{
ConnectionType::ADDR_FETCH,
};
constexpr auto ALL_NETWORKS = std::array{
Network::NET_UNROUTABLE,
Network::NET_IPV4,
Network::NET_IPV6,
Network::NET_ONION,
Network::NET_I2P,
Network::NET_CJDNS,
Network::NET_INTERNAL,
};
/**
* A mocked Sock alternative that returns a statically contained data upon read and succeeds
* and ignores all writes. The data to be returned is given to the constructor and when it is

View File

@ -5,6 +5,7 @@
"""Test dash-cli"""
from decimal import Decimal
import re
from test_framework.blocktools import COINBASE_MATURITY
from test_framework.test_framework import BitcoinTestFramework
@ -29,6 +30,41 @@ TOO_MANY_ARGS = 'error: too many arguments (maximum 2 for nblocks and maxtries)'
WALLET_NOT_LOADED = 'Requested wallet does not exist or is not loaded'
WALLET_NOT_SPECIFIED = 'Wallet file not specified'
def cli_get_info_string_to_dict(cli_get_info_string):
"""Helper method to convert human-readable -getinfo into a dictionary"""
cli_get_info = {}
lines = cli_get_info_string.splitlines()
line_idx = 0
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
while line_idx < len(lines):
# Remove ansi colour code
line = ansi_escape.sub('', lines[line_idx])
if "Balances" in line:
# When "Balances" appears in a line, all of the following lines contain "balance: wallet" until an empty line
cli_get_info["Balances"] = {}
while line_idx < len(lines) and not (lines[line_idx + 1] == ''):
line_idx += 1
balance, wallet = lines[line_idx].strip().split(" ")
# Remove right justification padding
wallet = wallet.strip()
if wallet == '""':
# Set default wallet("") to empty string
wallet = ''
cli_get_info["Balances"][wallet] = balance.strip()
elif ": " in line:
key, value = line.split(": ")
if key == 'Wallet' and value == '""':
# Set default wallet("") to empty string
value = ''
if key == "Proxies" and value == "n/a":
# Set N/A to empty string to represent no proxy
value = ''
cli_get_info[key.strip()] = value.strip()
line_idx += 1
return cli_get_info
class TestBitcoinCli(BitcoinTestFramework):
def set_test_params(self):
self.setup_clean_chain = True
@ -72,41 +108,51 @@ class TestBitcoinCli(BitcoinTestFramework):
self.log.info("Test -getinfo with arguments fails")
assert_raises_process_error(1, "-getinfo takes no arguments", self.nodes[0].cli('-getinfo').help)
self.log.info("Test -getinfo with -color=never does not return ANSI escape codes")
assert "\u001b[0m" not in self.nodes[0].cli('-getinfo', '-color=never').send_cli()
self.log.info("Test -getinfo with -color=always returns ANSI escape codes")
assert "\u001b[0m" in self.nodes[0].cli('-getinfo', '-color=always').send_cli()
self.log.info("Test -getinfo with invalid value for -color option")
assert_raises_process_error(1, "Invalid value for -color option. Valid values: always, auto, never.", self.nodes[0].cli('-getinfo', '-color=foo').send_cli)
self.log.info("Test -getinfo returns expected network and blockchain info")
if self.is_wallet_compiled():
self.nodes[0].encryptwallet(password)
cli_get_info = self.nodes[0].cli('-getinfo').send_cli()
cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli()
cli_get_info = cli_get_info_string_to_dict(cli_get_info_string)
network_info = self.nodes[0].getnetworkinfo()
blockchain_info = self.nodes[0].getblockchaininfo()
assert_equal(cli_get_info['version'], network_info['version'])
assert_equal(cli_get_info['blocks'], blockchain_info['blocks'])
assert_equal(cli_get_info['headers'], blockchain_info['headers'])
assert_equal(cli_get_info['timeoffset'], network_info['timeoffset'])
assert_equal(
cli_get_info['connections'],
{
'in': network_info['connections_in'],
'out': network_info['connections_out'],
'total': network_info['connections'],
'mn_in': network_info['connections_mn_in'],
'mn_out': network_info['connections_mn_out'],
'mn_total': network_info['connections_mn'],
}
)
assert_equal(cli_get_info['proxy'], network_info['networks'][0]['proxy'])
assert_equal(cli_get_info['difficulty'], blockchain_info['difficulty'])
assert_equal(cli_get_info['chain'], blockchain_info['chain'])
assert_equal(int(cli_get_info['Version']), network_info['version'])
assert_equal(cli_get_info['Verification progress'], "%.4f%%" % (blockchain_info['verificationprogress'] * 100))
assert_equal(int(cli_get_info['Blocks']), blockchain_info['blocks'])
assert_equal(int(cli_get_info['Headers']), blockchain_info['headers'])
assert_equal(int(cli_get_info['Time offset (s)']), network_info['timeoffset'])
expected_network_info = f"in {network_info['connections_in']}, out {network_info['connections_out']}, total {network_info['connections']}, mn_in {network_info['connections_mn_in']}, mn_out {network_info['connections_mn_out']}, mn_total {network_info['connections_mn']}"
assert_equal(cli_get_info["Network"], expected_network_info)
assert_equal(cli_get_info['Proxies'], network_info['networks'][0]['proxy'])
assert_equal(Decimal(cli_get_info['Difficulty']), blockchain_info['difficulty'])
assert_equal(cli_get_info['Chain'], blockchain_info['chain'])
self.log.info("Test -getinfo and dash-cli return all proxies")
self.restart_node(0, extra_args=["-proxy=127.0.0.1:9050", "-i2psam=127.0.0.1:7656"])
network_info = self.nodes[0].getnetworkinfo()
cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli()
cli_get_info = cli_get_info_string_to_dict(cli_get_info_string)
assert_equal(cli_get_info["Proxies"], "127.0.0.1:9050 (ipv4, ipv6, onion), 127.0.0.1:7656 (i2p)")
if self.is_wallet_compiled():
self.log.info("Test -getinfo and dash-cli getwalletinfo return expected wallet info")
assert_equal(cli_get_info['balance'], BALANCE)
assert 'balances' not in cli_get_info.keys()
assert_equal(Decimal(cli_get_info['Balance']), BALANCE)
assert 'Balances' not in cli_get_info_string
wallet_info = self.nodes[0].getwalletinfo()
assert_equal(cli_get_info['coinjoin_balance'], wallet_info['coinjoin_balance'])
assert_equal(cli_get_info['keypoolsize'], wallet_info['keypoolsize'])
assert_equal(cli_get_info['unlocked_until'], wallet_info['unlocked_until'])
assert_equal(cli_get_info['paytxfee'], wallet_info['paytxfee'])
assert_equal(cli_get_info['relayfee'], network_info['relayfee'])
assert_equal(Decimal(cli_get_info['CoinJoin balance']), wallet_info['coinjoin_balance'])
assert_equal(int(cli_get_info['Keypool size']), wallet_info['keypoolsize'])
assert_equal(int(cli_get_info['Unlocked until']), wallet_info['unlocked_until'])
assert_equal(Decimal(cli_get_info['Transaction fee rate (-paytxfee) (DASH/kB)']), wallet_info['paytxfee'])
assert_equal(Decimal(cli_get_info['Min tx relay fee rate (DASH/kB)']), network_info['relayfee'])
assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info)
# Setup to test -getinfo, -generate, and -rpcwallet= with multiple wallets.
@ -129,44 +175,57 @@ class TestBitcoinCli(BitcoinTestFramework):
self.log.info("Test -getinfo with multiple wallets and -rpcwallet returns specified wallet balance")
for i in range(len(wallets)):
cli_get_info = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[i])).send_cli()
assert 'balances' not in cli_get_info.keys()
assert_equal(cli_get_info['balance'], amounts[i])
cli_get_info_string = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[i])).send_cli()
cli_get_info = cli_get_info_string_to_dict(cli_get_info_string)
assert 'Balances' not in cli_get_info_string
assert_equal(cli_get_info["Wallet"], wallets[i])
assert_equal(Decimal(cli_get_info['Balance']), amounts[i])
self.log.info("Test -getinfo with multiple wallets and -rpcwallet=non-existing-wallet returns no balances")
cli_get_info_keys = self.nodes[0].cli('-getinfo', '-rpcwallet=does-not-exist').send_cli().keys()
assert 'balance' not in cli_get_info_keys
assert 'balances' not in cli_get_info_keys
cli_get_info_string = self.nodes[0].cli('-getinfo', '-rpcwallet=does-not-exist').send_cli()
assert 'Balance' not in cli_get_info_string
assert 'Balances' not in cli_get_info_string
self.log.info("Test -getinfo with multiple wallets returns all loaded wallet names and balances")
assert_equal(set(self.nodes[0].listwallets()), set(wallets))
cli_get_info = self.nodes[0].cli('-getinfo').send_cli()
assert 'balance' not in cli_get_info.keys()
assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets, amounts)})
cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli()
cli_get_info = cli_get_info_string_to_dict(cli_get_info_string)
assert 'Balance' not in cli_get_info
for k, v in zip(wallets, amounts):
assert_equal(Decimal(cli_get_info['Balances'][k]), v)
# Unload the default wallet and re-verify.
self.nodes[0].unloadwallet(wallets[0])
assert wallets[0] not in self.nodes[0].listwallets()
cli_get_info = self.nodes[0].cli('-getinfo').send_cli()
assert 'balance' not in cli_get_info.keys()
assert_equal(cli_get_info['balances'], {k: v for k, v in zip(wallets[1:], amounts[1:])})
cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli()
cli_get_info = cli_get_info_string_to_dict(cli_get_info_string)
assert 'Balance' not in cli_get_info
assert 'Balances' in cli_get_info_string
for k, v in zip(wallets[1:], amounts[1:]):
assert_equal(Decimal(cli_get_info['Balances'][k]), v)
assert wallets[0] not in cli_get_info
self.log.info("Test -getinfo after unloading all wallets except a non-default one returns its balance")
self.nodes[0].unloadwallet(wallets[2])
assert_equal(self.nodes[0].listwallets(), [wallets[1]])
cli_get_info = self.nodes[0].cli('-getinfo').send_cli()
assert 'balances' not in cli_get_info.keys()
assert_equal(cli_get_info['balance'], amounts[1])
cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli()
cli_get_info = cli_get_info_string_to_dict(cli_get_info_string)
assert 'Balances' not in cli_get_info_string
assert_equal(cli_get_info['Wallet'], wallets[1])
assert_equal(Decimal(cli_get_info['Balance']), amounts[1])
self.log.info("Test -getinfo with -rpcwallet=remaining-non-default-wallet returns only its balance")
cli_get_info = self.nodes[0].cli('-getinfo', rpcwallet2).send_cli()
assert 'balances' not in cli_get_info.keys()
assert_equal(cli_get_info['balance'], amounts[1])
cli_get_info_string = self.nodes[0].cli('-getinfo', rpcwallet2).send_cli()
cli_get_info = cli_get_info_string_to_dict(cli_get_info_string)
assert 'Balances' not in cli_get_info_string
assert_equal(cli_get_info['Wallet'], wallets[1])
assert_equal(Decimal(cli_get_info['Balance']), amounts[1])
self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balances")
cli_get_info_keys = self.nodes[0].cli('-getinfo', rpcwallet3).send_cli().keys()
assert 'balance' not in cli_get_info_keys
assert 'balances' not in cli_get_info_keys
cli_get_info_string = self.nodes[0].cli('-getinfo', rpcwallet3).send_cli()
cli_get_info_keys = cli_get_info_string_to_dict(cli_get_info_string)
assert 'Balance' not in cli_get_info_keys
assert 'Balances' not in cli_get_info_string
# Test bitcoin-cli -generate.
n1 = 3