diff --git a/doc/release-notes-18594.md b/doc/release-notes-18594.md new file mode 100644 index 0000000000..66d8450d86 --- /dev/null +++ b/doc/release-notes-18594.md @@ -0,0 +1,5 @@ +## CLI + +The `dash-cli -getinfo` command now displays the wallet name and balance for +each of the loaded wallets when more than one is loaded (e.g. in multiwallet +mode) and a wallet is not specified with `-rpcwallet`. (#18594) diff --git a/doc/tor.md b/doc/tor.md index 8e6d6be525..40245a516d 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -53,11 +53,12 @@ config file): *Needed for Tor version 0.2.7.0 and older versions of Tor only. Fo versions of Tor see [Section 4](#4-automatically-listen-on-tor).* HiddenServiceDir /var/lib/tor/dashcore-service/ - HiddenServicePort 9999 127.0.0.1:9999 - HiddenServicePort 19999 127.0.0.1:19999 + HiddenServicePort 9999 127.0.0.1:9996 + HiddenServicePort 19999 127.0.0.1:19996 -The directory can be different of course, but (both) port numbers should be equal to -your dashd's P2P listen port (9999 by default). +The directory can be different of course, but virtual port numbers should be equal to +your dashd's P2P listen port (9999 by default), and target addresses and ports +should be equal to binding address and port for inbound Tor connections (127.0.0.1:9996 by default). -externalip=X You can tell Dash Core about its publicly reachable address using this option, and this can be a .onion address. Given the above diff --git a/src/Makefile.am b/src/Makefile.am index f3d83a5908..ada9398bad 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -263,6 +263,7 @@ BITCOIN_CORE_H = \ reverse_iterator.h \ rpc/blockchain.h \ rpc/client.h \ + rpc/mining.h \ rpc/protocol.h \ rpc/rawtransaction_util.h \ rpc/register.h \ diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 53ae2ed867..599d33cde8 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,9 @@ static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900; static const bool DEFAULT_NAMED=false; static const int CONTINUE_EXECUTION=-1; +/** Default number of blocks to generate for RPC generatetoaddress. */ +static const std::string DEFAULT_NBLOCKS = "1"; + static void SetupCliArgs(ArgsManager& argsman) { SetupHelpOptions(argsman); @@ -50,7 +54,9 @@ static void SetupCliArgs(ArgsManager& argsman) 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("-generate", strprintf("Generate blocks immediately, equivalent to RPC generatenewaddress followed by RPC generatetoaddress. Optional positional integer arguments are number of blocks to generate (default: %s) and maximum iterations to try (default: %s), equivalent to RPC generatetoaddress nblocks and maxtries arguments. Example: dash-cli -generate 4 1000", DEFAULT_NBLOCKS, DEFAULT_MAX_TRIES), 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); argsman.AddArg("-named", strprintf("Pass named instead of positional arguments (default: %s)", DEFAULT_NAMED), 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("-rpcconnect=", strprintf("Send commands to node running on (default: %s)", DEFAULT_RPCCONNECT), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); @@ -239,6 +245,7 @@ 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 @@ -250,6 +257,7 @@ public: 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; } @@ -257,9 +265,9 @@ public: UniValue ProcessReply(const UniValue &batch_in) override { UniValue result(UniValue::VOBJ); - std::vector batch = JSONRPCProcessBatchReply(batch_in, 3); - // Errors in getnetworkinfo() and getblockchaininfo() are fatal, pass them on - // getwalletinfo() is allowed to fail in case there is no wallet. + 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]; } @@ -276,7 +284,6 @@ public: 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("balance", batch[ID_WALLETINFO]["result"]["balance"]); result.pushKV("coinjoin_balance", batch[ID_WALLETINFO]["result"]["coinjoin_balance"]); result.pushKV("keypoolsize", batch[ID_WALLETINFO]["result"]["keypoolsize"]); if (!batch[ID_WALLETINFO]["result"]["unlocked_until"].isNull()) { @@ -284,12 +291,207 @@ public: } 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: + static constexpr int8_t UNKNOWN_NETWORK{-1}; + static constexpr size_t m_networks_size{3}; + const std::array m_networks{{"ipv4", "ipv6", "onion"}}; + std::array, 3> m_counts{{{}}}; //!< Peer counts by (in/out/total, networks/total/block-relay) + int8_t NetworkStringToId(const std::string& str) const + { + for (size_t i = 0; i < m_networks_size; ++i) { + if (str == m_networks.at(i)) return i; + } + return UNKNOWN_NETWORK; + } + 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; } + bool m_is_asmap_on{false}; + size_t m_max_addr_length{0}; + size_t m_max_id_length{2}; + 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 network; + std::string sub_version; + 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::vector m_peers; + std::string ChainToString() const + { + if (gArgs.GetChainName() == CBaseChainParams::TESTNET) return " testnet"; + if (gArgs.GetChainName() == CBaseChainParams::REGTEST) return " regtest"; + return ""; + } + const int64_t m_time_now{GetSystemTimeInSeconds()}; + +public: + static constexpr int ID_PEERINFO = 0; + static constexpr 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() < 200000) { + throw std::runtime_error("-netinfo requires dashd server to be running v20.0 and up"); + } + + // Count peer connection totals, and if DetailsRequested(), store peer data in a vector of structs. + for (const UniValue& peer : batch[ID_PEERINFO]["result"].getValues()) { + const std::string network{peer["network"].get_str()}; + const int8_t network_id{NetworkStringToId(network)}; + if (network_id == UNKNOWN_NETWORK) continue; + const bool is_outbound{!peer["inbound"].get_bool()}; + const bool is_block_relay{!peer["relaytxes"].get_bool()}; + ++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 (DetailsRequested()) { + // Push data for this peer to the peers vector. + const int peer_id{peer["id"].get_int()}; + const int mapped_as{peer["mapped_as"].isNull() ? 0 : peer["mapped_as"].get_int()}; + const int version{peer["version"].get_int()}; + 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()}; + const std::string addr{peer["addr"].get_str()}; + const std::string sub_version{peer["subver"].get_str()}; + m_peers.push_back({peer_id, mapped_as, version, conn_time, last_blck, last_recv, last_send, last_trxn, min_ping, ping, addr, network, sub_version, is_block_relay, is_outbound}); + m_max_id_length = std::max(ToString(peer_id).length(), m_max_id_length); + m_max_addr_length = std::max(addr.length() + 1, m_max_addr_length); + m_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() && !m_peers.empty()) { + std::sort(m_peers.begin(), m_peers.end()); + result += "Peer connections sorted by direction and min ping\n<-> relay net mping ping send recv txn blk uptime "; + 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%6s%7s%5s%5s%5s%5s%7s%*i %*s %-*s%s\n", + peer.is_outbound ? "out" : "in", + peer.is_block_relay ? "block" : "full", + peer.network, + peer.min_ping == -1 ? "" : ToString(round(1000 * peer.min_ping)), + peer.ping == -1 ? "" : ToString(round(1000 * peer.ping)), + peer.last_send == 0 ? "" : ToString(m_time_now - peer.last_send), + peer.last_recv == 0 ? "" : ToString(m_time_now - peer.last_recv), + peer.last_trxn == 0 ? "" : ToString((m_time_now - peer.last_trxn) / 60), + peer.last_blck == 0 ? "" : ToString((m_time_now - peer.last_blck) / 60), + peer.conn_time == 0 ? "" : ToString((m_time_now - peer.conn_time) / 60), + m_is_asmap_on ? 7 : 0, // variable spacing + m_is_asmap_on && peer.mapped_as != 0 ? ToString(peer.mapped_as) : "", + m_max_id_length, // variable spacing + peer.id, + IsAddressSelected() ? m_max_addr_length : 0, // variable spacing + IsAddressSelected() ? peer.addr : "", + IsVersionSelected() && version != "0" ? version : ""); + } + result += " ms ms sec sec min min min\n\n"; + } + + // Report peer connection totals by type. + result += " ipv4 ipv6 onion total block-relay\n"; + const std::array rows{{"in", "out", "total"}}; + for (size_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)); + } + + // 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 RPC generatetoaddress request. */ +class GenerateToAddressRequestHandler : public BaseRequestHandler +{ +public: + UniValue PrepareRequest(const std::string& method, const std::vector& args) override + { + address_str = args.at(1); + UniValue params{RPCConvertValues("generatetoaddress", args)}; + return JSONRPCRequestObj("generatetoaddress", params, 1); + } + + UniValue ProcessReply(const UniValue &reply) override + { + UniValue result(UniValue::VOBJ); + result.pushKV("address", address_str); + result.pushKV("blocks", reply.get_obj()["result"]); + return JSONRPCReplyObj(result, NullUniValue, 1); + } +protected: + std::string address_str; +}; + /** Process default single requests */ class DefaultRequestHandler: public BaseRequestHandler { public: @@ -310,7 +512,7 @@ public: } }; -static UniValue CallRPC(BaseRequestHandler *rh, const std::string& strMethod, const std::vector& args) +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: @@ -376,14 +578,12 @@ static UniValue CallRPC(BaseRequestHandler *rh, const std::string& strMethod, co // check if we should use a special wallet endpoint std::string endpoint = "/"; - if (!gArgs.GetArgs("-rpcwallet").empty()) { - std::string walletName = gArgs.GetArg("-rpcwallet", ""); - char *encodedURI = evhttp_uriencode(walletName.data(), walletName.size(), false); + if (rpcwallet) { + char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false); if (encodedURI) { - endpoint = "/wallet/"+ std::string(encodedURI); + endpoint = "/wallet/" + std::string(encodedURI); free(encodedURI); - } - else { + } else { throw CConnectionFailed("uri-encode failed"); } } @@ -427,6 +627,121 @@ static UniValue CallRPC(BaseRequestHandler *rh, const std::string& strMethod, co 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; +} + +/** Parse UniValue result to update the message to print to std::cout. */ +static void ParseResult(const UniValue& result, std::string& strPrint) +{ + if (result.isNull()) return; + strPrint = result.isStr() ? result.get_str() : result.write(2); +} + +/** Parse UniValue error to update the message to print to std::cerr and the code to return. */ +static void ParseError(const UniValue& error, std::string& strPrint, int& nRet) +{ + if (error.isObject()) { + const UniValue& err_code = find_value(error, "code"); + const UniValue& err_msg = find_value(error, "message"); + if (!err_code.isNull()) { + strPrint = "error code: " + err_code.getValStr() + "\n"; + } + if (err_msg.isStr()) { + strPrint += ("error message:\n" + err_msg.get_str()); + } + if (err_code.isNum() && err_code.get_int() == RPC_WALLET_NOT_SPECIFIED) { + strPrint += "\nTry adding \"-rpcwallet=\" option to dash-cli command line."; + } + } else { + strPrint = "error: " + error.write(); + } + nRet = abs(error["code"].get_int()); +} + +/** + * 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. + */ +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); +} + +/** + * Call RPC getnewaddress. + * @returns getnewaddress response as a UniValue object. + */ +static UniValue GetNewAddress() +{ + std::optional wallet_name{}; + if (gArgs.IsArgSet("-rpcwallet")) wallet_name = gArgs.GetArg("-rpcwallet", ""); + std::unique_ptr rh{std::make_unique()}; + return ConnectAndCallRPC(rh.get(), "getnewaddress", /* args=*/{}, wallet_name); +} + +/** + * Check bounds and set up args for RPC generatetoaddress params: nblocks, address, maxtries. + * @param[in] address Reference to const string address to insert into the args. + * @param args Reference to vector of string args to modify. + */ +static void SetGenerateToAddressArgs(const std::string& address, std::vector& args) +{ + if (args.size() > 2) throw std::runtime_error("too many arguments (maximum 2 for nblocks and maxtries)"); + if (args.size() == 0) { + args.emplace_back(DEFAULT_NBLOCKS); + } else if (args.at(0) == "0") { + throw std::runtime_error("the first argument (number of blocks to generate, default: " + DEFAULT_NBLOCKS + ") must be an integer value greater than zero"); + } + args.emplace(args.begin() + 1, address); +} + static int CommandLineRPC(int argc, char *argv[]) { std::string strPrint; @@ -483,9 +798,19 @@ static int CommandLineRPC(int argc, char *argv[]) } std::unique_ptr rh; std::string method; - if (gArgs.GetBoolArg("-getinfo", false)) { + if (gArgs.IsArgSet("-getinfo")) { rh.reset(new GetinfoRequestHandler()); - method = ""; + } else if (gArgs.GetBoolArg("-netinfo", false)) { + rh.reset(new NetinfoRequestHandler()); + } else if (gArgs.GetBoolArg("-generate", false)) { + const UniValue getnewaddress{GetNewAddress()}; + const UniValue& error{find_value(getnewaddress, "error")}; + if (error.isNull()) { + SetGenerateToAddressArgs(find_value(getnewaddress, "result").get_str(), args); + rh.reset(new GenerateToAddressRequestHandler()); + } else { + ParseError(error, strPrint, nRet); + } } else { rh.reset(new DefaultRequestHandler()); if (args.size() < 1) { @@ -494,62 +819,28 @@ static int CommandLineRPC(int argc, char *argv[]) method = args[0]; args.erase(args.begin()); // Remove trailing method name from arguments vector } + if (nRet == 0) { + // Perform RPC call + std::optional wallet_name{}; + if (gArgs.IsArgSet("-rpcwallet")) wallet_name = gArgs.GetArg("-rpcwallet", ""); + const UniValue reply = ConnectAndCallRPC(rh.get(), method, args, wallet_name); - // Execute and handle connection failures with -rpcwait - const bool fWait = gArgs.GetBoolArg("-rpcwait", false); - do { - try { - const UniValue reply = CallRPC(rh.get(), method, args); - - // Parse reply - const UniValue& result = find_value(reply, "result"); - const UniValue& error = find_value(reply, "error"); - - if (!error.isNull()) { - // Error - int code = error["code"].get_int(); - if (fWait && code == RPC_IN_WARMUP) - throw CConnectionFailed("server in warmup"); - strPrint = "error: " + error.write(); - nRet = abs(code); - if (error.isObject()) - { - UniValue errCode = find_value(error, "code"); - 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 dash-cli command line."; - } - } - } else { - // Result - if (result.isNull()) - strPrint = ""; - else if (result.isStr()) - strPrint = result.get_str(); - else - strPrint = result.write(2); + // Parse reply + 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 } - // Connection succeeded, no need to retry. - break; + ParseResult(result, strPrint); + } else { + ParseError(error, strPrint, nRet); } - catch (const CConnectionFailed&) { - if (fWait) - UninterruptibleSleep(std::chrono::milliseconds{1000}); - else - throw; - } - } while (fWait); - } - catch (const std::exception& e) { + } + } catch (const std::exception& e) { strPrint = std::string("error: ") + e.what(); nRet = EXIT_FAILURE; - } - catch (...) { + } catch (...) { PrintExceptionContinue(std::current_exception(), "CommandLineRPC()"); throw; } diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index 926dd85023..74a9039c6f 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -49,16 +49,20 @@ const CBaseChainParams& BaseParams() return *globalChainBaseParams; } +/** + * Port numbers for incoming Tor connections (9996, 19996, 19796, 19896) have + * been chosen arbitrarily to keep ranges of used ports tight. + */ std::unique_ptr CreateBaseChainParams(const std::string& chain) { if (chain == CBaseChainParams::MAIN) - return std::make_unique("", 9998); + return std::make_unique("", 9998, 9996); else if (chain == CBaseChainParams::TESTNET) - return std::make_unique("testnet3", 19998); + return std::make_unique("testnet3", 19998, 19996); else if (chain == CBaseChainParams::DEVNET) - return std::make_unique(gArgs.GetDevNetName(), 19798); + return std::make_unique(gArgs.GetDevNetName(), 19798, 19796); else if (chain == CBaseChainParams::REGTEST) - return std::make_unique("regtest", 19898); + return std::make_unique("regtest", 19898, 19896); else throw std::runtime_error(strprintf("%s: Unknown chain %s.", __func__, chain)); } diff --git a/src/chainparamsbase.h b/src/chainparamsbase.h index 65580dd80d..298577923c 100644 --- a/src/chainparamsbase.h +++ b/src/chainparamsbase.h @@ -26,13 +26,16 @@ public: ///@} const std::string& DataDir() const { return strDataDir; } - uint16_t RPCPort() const { return nRPCPort; } + uint16_t RPCPort() const { return m_rpc_port; } + uint16_t OnionServiceTargetPort() const { return m_onion_service_target_port; } CBaseChainParams() = delete; - CBaseChainParams(const std::string& data_dir, int rpc_port) : nRPCPort(rpc_port), strDataDir(data_dir) {} + CBaseChainParams(const std::string& data_dir, uint16_t rpc_port, uint16_t onion_service_target_port) + : m_rpc_port(rpc_port), m_onion_service_target_port(onion_service_target_port), strDataDir(data_dir) {} private: - uint16_t nRPCPort; + const uint16_t m_rpc_port; + const uint16_t m_onion_service_target_port; std::string strDataDir; }; diff --git a/src/init.cpp b/src/init.cpp index 00f8025e4a..fd758a1201 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -552,7 +552,7 @@ void SetupServerArgs(NodeContext& node) argsman.AddArg("-allowprivatenet", strprintf("Allow RFC1918 addresses to be relayed and connected to (default: %u)", DEFAULT_ALLOWPRIVATENET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-banscore=", strprintf("Threshold for disconnecting and discouraging misbehaving peers (default: %u)", DEFAULT_BANSCORE_THRESHOLD), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bantime=", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); - argsman.AddArg("-bind=", "Bind to given address and always listen on it. Use [host]:port notation for IPv6", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); + argsman.AddArg("-bind=[:][=onion]", strprintf("Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections (default: 127.0.0.1:%u=onion, testnet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion)", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-connect=", "Connect only to the specified node; -noconnect disables automatic connections (the rules for this peer are the same as for -addnode). This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-discover", "Discover own IP addresses (default: 1 when listening and no -externalip or -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-dns", strprintf("Allow DNS lookups for -addnode, -seednode and -connect (default: %u)", DEFAULT_NAME_LOOKUP), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -2480,8 +2480,6 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc } } LogPrintf("::ChainActive().Height() = %d\n", chain_active_height); - if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) - StartTorControl(); Discover(); @@ -2506,13 +2504,39 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc connOptions.nMaxOutboundLimit = 1024 * 1024 * args.GetArg("-maxuploadtarget", DEFAULT_MAX_UPLOAD_TARGET); connOptions.m_peer_connect_timeout = peer_connect_timeout; - for (const std::string& strBind : args.GetArgs("-bind")) { - CService addrBind; - if (!Lookup(strBind, addrBind, GetListenPort(), false)) { - return InitError(ResolveErrMsg("bind", strBind)); + for (const std::string& bind_arg : args.GetArgs("-bind")) { + CService bind_addr; + const size_t index = bind_arg.rfind('='); + if (index == std::string::npos) { + if (Lookup(bind_arg, bind_addr, GetListenPort(), false)) { + connOptions.vBinds.push_back(bind_addr); + continue; + } + } else { + const std::string network_type = bind_arg.substr(index + 1); + if (network_type == "onion") { + const std::string truncated_bind_arg = bind_arg.substr(0, index); + if (Lookup(truncated_bind_arg, bind_addr, BaseParams().OnionServiceTargetPort(), false)) { + connOptions.onion_binds.push_back(bind_addr); + continue; + } + } } - connOptions.vBinds.push_back(addrBind); + return InitError(ResolveErrMsg("bind", bind_arg)); } + + if (connOptions.onion_binds.empty()) { + connOptions.onion_binds.push_back(DefaultOnionServiceTarget()); + } + + if (args.GetBoolArg("-listenonion", DEFAULT_LISTEN_ONION)) { + const auto bind_addr = connOptions.onion_binds.front(); + if (connOptions.onion_binds.size() > 1) { + InitWarning(strprintf(_("More than one onion bind address is provided. Using %s for the automatically created Tor onion service."), bind_addr.ToStringIPPort())); + } + StartTorControl(bind_addr); + } + for (const std::string& strBind : args.GetArgs("-whitebind")) { NetWhitebindPermissions whitebind; bilingual_str error; diff --git a/src/net.cpp b/src/net.cpp index 51bf6c3c0f..ac5c0e9b98 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -54,6 +54,7 @@ #include #endif +#include #include #include @@ -99,6 +100,11 @@ enum BindFlags { BF_NONE = 0, BF_EXPLICIT = (1U << 0), BF_REPORT_ERROR = (1U << 1), + /** + * Do not call AddLocal() for our special addresses, e.g., for incoming + * Tor connections, to prevent gossiping them over the network. + */ + BF_DONT_ADVERTISE = (1U << 2), }; #ifndef USE_WAKEUP_PIPE @@ -580,6 +586,11 @@ std::string CNode::GetLogString() const return fLogIPs ? addr.ToString() : strprintf("%d", id); } +Network CNode::ConnectedThroughNetwork() const +{ + return fInbound && m_inbound_onion ? NET_ONION : addr.GetNetClass(); +} + #undef X #define X(name) stats.name = name void CNode::copyStats(CNodeStats &stats, const std::vector &m_asmap) @@ -588,6 +599,7 @@ void CNode::copyStats(CNodeStats &stats, const std::vector &m_asmap) X(nServices); X(addr); X(addrBind); + stats.m_network = GetNetworkName(ConnectedThroughNetwork()); stats.m_mapped_as = addr.GetMappedAS(m_asmap); if (!m_block_relay_only_peer) { LOCK(m_tx_relay->cs_filter); @@ -1199,7 +1211,9 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { if (NetPermissions::HasFlag(permissionFlags, PF_BLOOMFILTER)) { nodeServices = static_cast(nodeServices | NODE_BLOOM); } - CNode* pnode = new CNode(id, nodeServices, GetBestHeight(), hSocket, addr, CalculateKeyedNetGroup(addr), nonce, addr_bind, "", true); + + const bool inbound_onion = std::find(m_onion_binds.begin(), m_onion_binds.end(), addr_bind) != m_onion_binds.end(); + CNode* pnode = new CNode(id, nodeServices, GetBestHeight(), hSocket, addr, CalculateKeyedNetGroup(addr), nonce, addr_bind, "", true, inbound_onion); pnode->AddRef(); pnode->m_permissionFlags = permissionFlags; // If this flag is present, the user probably expect that RPC and QT report it as whitelisted (backward compatibility) @@ -2875,9 +2889,6 @@ bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError, vhListenSocket.push_back(ListenSocket(sock->Release(), permissions)); - if (addrBind.IsRoutable() && fDiscover && (permissions & PF_NOBAN) == 0) - AddLocal(addrBind, LOCAL_BIND); - return true; } @@ -2974,10 +2985,18 @@ bool CConnman::Bind(const CService &addr, unsigned int flags, NetPermissionFlags } return false; } + + if (addr.IsRoutable() && fDiscover && !(flags & BF_DONT_ADVERTISE) && !(permissions & PF_NOBAN)) { + AddLocal(addr, LOCAL_BIND); + } + return true; } -bool CConnman::InitBinds(const std::vector& binds, const std::vector& whiteBinds) +bool CConnman::InitBinds( + const std::vector& binds, + const std::vector& whiteBinds, + const std::vector& onion_binds) { bool fBound = false; for (const auto& addrBind : binds) { @@ -2988,11 +3007,16 @@ bool CConnman::InitBinds(const std::vector& binds, const std::vectorThreadSafeMessageBox( _("Failed to listen on any port. Use -listen=0 if you want this."), @@ -3741,7 +3765,7 @@ int CConnman::GetBestHeight() const unsigned int CConnman::GetReceiveFloodSize() const { return nReceiveFloodSize; } -CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, int nMyStartingHeightIn, SOCKET hSocketIn, const CAddress& addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress& addrBindIn, const std::string& addrNameIn, bool fInboundIn, bool block_relay_only) +CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, int nMyStartingHeightIn, SOCKET hSocketIn, const CAddress& addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress& addrBindIn, const std::string& addrNameIn, bool fInboundIn, bool block_relay_only, bool inbound_onion) : nTimeConnected(GetSystemTimeInSeconds()), addr(addrIn), addrBind(addrBindIn), @@ -3752,7 +3776,8 @@ CNode::CNode(NodeId idIn, ServiceFlags nLocalServicesIn, int nMyStartingHeightIn id(idIn), nLocalHostNonce(nLocalHostNonceIn), nLocalServices(nLocalServicesIn), - nMyStartingHeight(nMyStartingHeightIn) + nMyStartingHeight(nMyStartingHeightIn), + m_inbound_onion(inbound_onion) { hSocket = hSocketIn; addrName = addrNameIn == "" ? addr.ToStringIPPort() : addrNameIn; diff --git a/src/net.h b/src/net.h index 773b19b3bd..55d6dfa1b4 100644 --- a/src/net.h +++ b/src/net.h @@ -178,6 +178,7 @@ public: std::vector vWhitelistedRange; std::vector vWhiteBinds; std::vector vBinds; + std::vector onion_binds; bool m_use_addrman_outgoing = true; std::vector m_specified_outgoing; std::vector m_added_nodes; @@ -211,6 +212,7 @@ public: vAddedNodes = connOptions.m_added_nodes; } socketEventsMode = connOptions.socketEventsMode; + m_onion_binds = connOptions.onion_binds; } CConnman(uint64_t seed0, uint64_t seed1, CAddrMan& addrman); @@ -496,7 +498,11 @@ private: bool BindListenPort(const CService& bindAddr, bilingual_str& strError, NetPermissionFlags permissions); bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions); - bool InitBinds(const std::vector& binds, const std::vector& whiteBinds); + bool InitBinds( + const std::vector& binds, + const std::vector& whiteBinds, + const std::vector& onion_binds); + void ThreadOpenAddedConnections(); void AddOneShot(const std::string& strDest); void ProcessOneShot(); @@ -675,6 +681,12 @@ private: std::atomic m_next_send_inv_to_incoming{0}; + /** + * A vector of -bind=
:=onion arguments each of which is + * an address and port that are designated for incoming Tor connections. + */ + std::vector m_onion_binds; + friend struct CConnmanTest; friend struct ConnmanTestMsg; }; @@ -797,6 +809,8 @@ public: CAddress addr; // Bind address of our side of the connection CAddress addrBind; + // Name of the network the peer connected through + std::string m_network; uint32_t m_mapped_as; // In case this is a verified MN, this value is the proTx of the MN uint256 verifiedProRegTxHash; @@ -1010,6 +1024,18 @@ public: std::atomic_bool fHasRecvData{false}; std::atomic_bool fCanSendData{false}; + /** + * Get network the peer connected through. + * + * Returns Network::NET_ONION for *inbound* onion connections, + * and CNetAddr::GetNetClass() otherwise. The latter cannot be used directly + * because it doesn't detect the former, and it's not the responsibility of + * the CNetAddr class to know the actual network a peer is connected through. + * + * @return network the peer connected through. + */ + Network ConnectedThroughNetwork() const; + protected: mapMsgCmdSize mapSendBytesPerMsgCmd; mapMsgCmdSize mapRecvBytesPerMsgCmd GUARDED_BY(cs_vRecv); @@ -1112,7 +1138,7 @@ public: std::set orphan_work_set; - CNode(NodeId id, ServiceFlags nLocalServicesIn, int nMyStartingHeightIn, SOCKET hSocketIn, const CAddress &addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress &addrBindIn, const std::string &addrNameIn = "", bool fInboundIn = false, bool block_relay_only = false); + CNode(NodeId id, ServiceFlags nLocalServicesIn, int nMyStartingHeightIn, SOCKET hSocketIn, const CAddress &addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, const CAddress &addrBindIn, const std::string &addrNameIn = "", bool fInboundIn = false, bool block_relay_only = false, bool inbound_onion = false); ~CNode(); CNode(const CNode&) = delete; CNode& operator=(const CNode&) = delete; @@ -1150,6 +1176,9 @@ private: CService addrLocal GUARDED_BY(cs_addrLocal); mutable CCriticalSection cs_addrLocal; + //! Whether this peer connected via our Tor onion service. + const bool m_inbound_onion{false}; + // Challenge sent in VERSION to be answered with MNAUTH (only happens between MNs) mutable CCriticalSection cs_mnauth; uint256 sentMNAuthChallenge GUARDED_BY(cs_mnauth); diff --git a/src/netaddress.cpp b/src/netaddress.cpp index ce3a3745d6..78e7fa871b 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -668,7 +668,7 @@ uint32_t CNetAddr::GetLinkedIPv4() const assert(false); } -uint32_t CNetAddr::GetNetClass() const +Network CNetAddr::GetNetClass() const { // Make sure that if we return NET_IPV6, then IsIPv6() is true. The callers expect that. diff --git a/src/netaddress.h b/src/netaddress.h index c771f760e3..708a91c074 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -200,7 +200,7 @@ class CNetAddr std::string ToStringIP(bool fUseGetnameinfo = true) const; uint64_t GetHash() const; bool GetInAddr(struct in_addr* pipv4Addr) const; - uint32_t GetNetClass() const; + Network GetNetClass() const; //! For IPv4, mapped IPv4, SIIT translated IPv4, Teredo, 6to4 tunneled addresses, return the relevant IPv4 address as a uint32. uint32_t GetLinkedIPv4() const; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index bb1afe616a..4d1eb11979 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -250,7 +250,7 @@ UniValue ParseNonRFCJSONValue(const std::string& strVal) UniValue jVal; if (!jVal.read(std::string("[")+strVal+std::string("]")) || !jVal.isArray() || jVal.size()!=1) - throw std::runtime_error(std::string("Error parsing JSON:")+strVal); + throw std::runtime_error(std::string("Error parsing JSON: ") + strVal); return jVal[0]; } diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 7d04413ac5..6e540ee417 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include