From 668baf262fbf08e98ab48bbc03057148d76bf6b6 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kittywhiskers@users.noreply.github.com> Date: Sun, 16 Apr 2023 07:18:51 +0000 Subject: [PATCH 01/11] merge bitcoin#18574: call getbalances.ismine.trusted instead of getwalletinfo.balance --- src/bitcoin-cli.cpp | 12 ++++++++---- test/functional/interface_bitcoin_cli.py | 19 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 53ae2ed867..4954542002 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -239,6 +239,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 +251,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 +259,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. + std::vector batch = JSONRPCProcessBatchReply(batch_in, batch_in.size()); + // 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 +278,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,6 +285,9 @@ 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); diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 101115d690..14e2e4fd76 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -6,6 +6,12 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_process_error, get_auth_cookie +# The block reward of coinbaseoutput.nValue (500) DASH/block matures after +# COINBASE_MATURITY (100) blocks. Therefore, after mining 101 blocks we expect +# node 0 to have a balance of (BLOCKS - COINBASE_MATURITY) * 500 DASH/block. +BLOCKS = 101 +BALANCE = (BLOCKS - 100) * 500 + class TestBitcoinCli(BitcoinTestFramework): def set_test_params(self): @@ -17,6 +23,7 @@ class TestBitcoinCli(BitcoinTestFramework): def run_test(self): """Main test logic""" + self.nodes[0].generate(BLOCKS) cli_response = self.nodes[0].cli("-version").send_cli() assert "{} RPC client version".format(self.config['environment']['PACKAGE_NAME']) in cli_response @@ -36,7 +43,7 @@ class TestBitcoinCli(BitcoinTestFramework): user, password = get_auth_cookie(self.nodes[0].datadir, self.chain) self.log.info("Test -stdinrpcpass option") - assert_equal(0, self.nodes[0].cli('-rpcuser=%s' % user, '-stdinrpcpass', input=password).getblockcount()) + assert_equal(BLOCKS, self.nodes[0].cli('-rpcuser=%s' % user, '-stdinrpcpass', input=password).getblockcount()) assert_raises_process_error(1, "Incorrect rpcuser or rpcpassword", self.nodes[0].cli('-rpcuser=%s' % user, '-stdinrpcpass', input="foo").echo) self.log.info("Test -stdin and -stdinrpcpass") @@ -52,10 +59,8 @@ class TestBitcoinCli(BitcoinTestFramework): self.log.info("Make sure that -getinfo with arguments fails") assert_raises_process_error(1, "-getinfo takes no arguments", self.nodes[0].cli('-getinfo').help) - self.log.info("Compare responses from `dash-cli -getinfo` and the RPCs data is retrieved from.") + self.log.info("Test that -getinfo returns the expected network and blockchain info") cli_get_info = self.nodes[0].cli('-getinfo').send_cli() - if self.is_wallet_compiled(): - wallet_info = self.nodes[0].getwalletinfo() network_info = self.nodes[0].getnetworkinfo() blockchain_info = self.nodes[0].getblockchaininfo() @@ -67,12 +72,16 @@ class TestBitcoinCli(BitcoinTestFramework): assert_equal(cli_get_info['difficulty'], blockchain_info['difficulty']) assert_equal(cli_get_info['chain'], blockchain_info['chain']) if self.is_wallet_compiled(): - assert_equal(cli_get_info['balance'], wallet_info['balance']) + self.log.info("Test that -getinfo returns the expected wallet info") + assert_equal(cli_get_info['balance'], BALANCE) + 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['paytxfee'], wallet_info['paytxfee']) assert_equal(cli_get_info['relayfee'], network_info['relayfee']) # unlocked_until is not tested because the wallet is not encrypted + else: + self.log.info("*** Wallet not compiled; -getinfo wallet tests skipped") if __name__ == '__main__': From 6c5eda9ad8db1c3851f35e2d61955a645ea26217 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kittywhiskers@users.noreply.github.com> Date: Sun, 16 Apr 2023 07:30:32 +0000 Subject: [PATCH 02/11] merge bitcoin#18653: add coverage for bitcoin-cli -rpcwait --- test/functional/interface_bitcoin_cli.py | 41 +++++++++++++++--------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 14e2e4fd76..3225001c57 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -25,16 +25,6 @@ class TestBitcoinCli(BitcoinTestFramework): """Main test logic""" self.nodes[0].generate(BLOCKS) - cli_response = self.nodes[0].cli("-version").send_cli() - assert "{} RPC client version".format(self.config['environment']['PACKAGE_NAME']) in cli_response - - self.log.info("Compare responses from getwalletinfo RPC and `dash-cli getwalletinfo`") - if self.is_wallet_compiled(): - self.nodes[0].createwallet(self.default_wallet_name) - cli_response = self.nodes[0].cli.getwalletinfo() - rpc_response = self.nodes[0].getwalletinfo() - assert_equal(cli_response, rpc_response) - self.log.info("Compare responses from getblockchaininfo RPC and `dash-cli getblockchaininfo`") cli_response = self.nodes[0].cli.getblockchaininfo() rpc_response = self.nodes[0].getblockchaininfo() @@ -56,32 +46,51 @@ class TestBitcoinCli(BitcoinTestFramework): self.log.info("Test connecting with non-existing RPC cookie file") assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo) - self.log.info("Make sure that -getinfo with arguments fails") + 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 that -getinfo returns the expected network and blockchain info") + 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() 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'], network_info['connections']) 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']) + if self.is_wallet_compiled(): - self.log.info("Test that -getinfo returns the expected wallet info") + self.log.info("Test -getinfo and dash-cli getwalletinfo return expected wallet info") assert_equal(cli_get_info['balance'], BALANCE) 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']) - # unlocked_until is not tested because the wallet is not encrypted + assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) else: - self.log.info("*** Wallet not compiled; -getinfo wallet tests skipped") + self.log.info("*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") + + self.stop_node(0) + + self.log.info("Test -version with node stopped") + cli_response = self.nodes[0].cli("-version").send_cli() + assert "{} RPC client version".format(self.config['environment']['PACKAGE_NAME']) in cli_response + + self.log.info("Test -rpcwait option waits for RPC connection instead of failing") + # Start node without RPC connection. + self.nodes[0].start() + # Verify failure without -rpcwait. + assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('getblockcount').echo) + # Verify success using -rpcwait. + assert_equal(BLOCKS, self.nodes[0].cli('-rpcwait', 'getblockcount').send_cli()) + self.nodes[0].wait_for_rpc_connection() if __name__ == '__main__': From 0d8932c979b664575281d1ce63be1347e5603afb Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kittywhiskers@users.noreply.github.com> Date: Sat, 18 Apr 2020 17:03:13 +0200 Subject: [PATCH 03/11] merge bitcoin#18691: add wait_for_cookie_credentials() to framework for rpcwait tests --- test/functional/interface_bitcoin_cli.py | 28 +++++++++------------ test/functional/test_framework/test_node.py | 18 ++++++++++++- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 3225001c57..36e187d040 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -13,7 +13,6 @@ BLOCKS = 101 BALANCE = (BLOCKS - 100) * 500 class TestBitcoinCli(BitcoinTestFramework): - def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 @@ -33,12 +32,12 @@ class TestBitcoinCli(BitcoinTestFramework): user, password = get_auth_cookie(self.nodes[0].datadir, self.chain) self.log.info("Test -stdinrpcpass option") - assert_equal(BLOCKS, self.nodes[0].cli('-rpcuser=%s' % user, '-stdinrpcpass', input=password).getblockcount()) - assert_raises_process_error(1, "Incorrect rpcuser or rpcpassword", self.nodes[0].cli('-rpcuser=%s' % user, '-stdinrpcpass', input="foo").echo) + assert_equal(BLOCKS, self.nodes[0].cli('-rpcuser={}'.format(user), '-stdinrpcpass', input=password).getblockcount()) + assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli('-rpcuser={}'.format(user), '-stdinrpcpass', input='foo').echo) self.log.info("Test -stdin and -stdinrpcpass") - assert_equal(["foo", "bar"], self.nodes[0].cli('-rpcuser=%s' % user, '-stdin', '-stdinrpcpass', input=password + "\nfoo\nbar").echo()) - assert_raises_process_error(1, "Incorrect rpcuser or rpcpassword", self.nodes[0].cli('-rpcuser=%s' % user, '-stdin', '-stdinrpcpass', input="foo").echo) + assert_equal(['foo', 'bar'], self.nodes[0].cli('-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input=password + '\nfoo\nbar').echo()) + assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli('-rpcuser={}'.format(user), '-stdin', '-stdinrpcpass', input='foo').echo) self.log.info("Test connecting to a non-existing server") assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo) @@ -52,7 +51,7 @@ class TestBitcoinCli(BitcoinTestFramework): 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 = self.nodes[0].cli().send_cli('-getinfo') network_info = self.nodes[0].getnetworkinfo() blockchain_info = self.nodes[0].getblockchaininfo() assert_equal(cli_get_info['version'], network_info['version']) @@ -77,20 +76,17 @@ class TestBitcoinCli(BitcoinTestFramework): else: self.log.info("*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") - self.stop_node(0) - self.log.info("Test -version with node stopped") - cli_response = self.nodes[0].cli("-version").send_cli() + self.stop_node(0) + cli_response = self.nodes[0].cli().send_cli('-version') assert "{} RPC client version".format(self.config['environment']['PACKAGE_NAME']) in cli_response - self.log.info("Test -rpcwait option waits for RPC connection instead of failing") - # Start node without RPC connection. - self.nodes[0].start() - # Verify failure without -rpcwait. - assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('getblockcount').echo) - # Verify success using -rpcwait. - assert_equal(BLOCKS, self.nodes[0].cli('-rpcwait', 'getblockcount').send_cli()) + self.log.info("Test -rpcwait option successfully waits for RPC connection") + self.nodes[0].start() # start node without RPC connection + self.nodes[0].wait_for_cookie_credentials() # ensure cookie file is available to avoid race condition + blocks = self.nodes[0].cli('-rpcwait').send_cli('getblockcount') self.nodes[0].wait_for_rpc_connection() + assert_equal(blocks, BLOCKS) if __name__ == '__main__': diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 004a169e2b..644418e26b 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -27,6 +27,7 @@ from .util import ( MAX_NODES, append_config, delete_cookie_file, + get_auth_cookie, get_rpc_proxy, rpc_url, wait_until, @@ -280,12 +281,27 @@ class TestNode(): pass # Port not yet open? else: raise # unknown OS error - except ValueError as e: # cookie file not found and no rpcuser or rpcassword. dashd still starting + except ValueError as e: # cookie file not found and no rpcuser or rpcpassword; dashd is still starting if "No RPC credentials" not in str(e): raise time.sleep(1.0 / poll_per_s) self._raise_assertion_error("Unable to connect to dashd after {}s".format(self.rpc_timeout)) + def wait_for_cookie_credentials(self): + """Ensures auth cookie credentials can be read, e.g. for testing CLI with -rpcwait before RPC connection is up.""" + self.log.debug("Waiting for cookie credentials") + # Poll at a rate of four times per second. + poll_per_s = 4 + for _ in range(poll_per_s * self.rpc_timeout): + try: + get_auth_cookie(self.datadir, self.chain) + self.log.debug("Cookie credentials successfully retrieved") + return + except ValueError: # cookie file not found and no rpcuser or rpcpassword; bitcoind is still starting + pass # so we continue polling until RPC credentials are retrieved + time.sleep(1.0 / poll_per_s) + self._raise_assertion_error("Unable to retrieve cookie credentials after {}s".format(self.rpc_timeout)) + def generate(self, nblocks, maxtries=1000000): self.log.debug("TestNode.generate() dispatches `generate` call to `generatetoaddress`") return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries) From 50b4901bce3960c5643fea406431037d766edcb7 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kittywhiskers@users.noreply.github.com> Date: Tue, 21 Apr 2020 18:24:17 +0200 Subject: [PATCH 04/11] merge bitcoin#18724: add coverage for -rpcwallet cli option --- test/functional/interface_bitcoin_cli.py | 43 +++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 36e187d040..148652e390 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -3,6 +3,7 @@ # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test dash-cli""" +from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_process_error, get_auth_cookie @@ -73,8 +74,48 @@ class TestBitcoinCli(BitcoinTestFramework): assert_equal(cli_get_info['paytxfee'], wallet_info['paytxfee']) assert_equal(cli_get_info['relayfee'], network_info['relayfee']) assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) + + # Setup to test -getinfo and -rpcwallet= with multiple wallets. + wallets = ['', 'Encrypted', 'secret'] + amounts = [Decimal('59.999928'), Decimal(9), Decimal(31)] + self.nodes[0].createwallet(wallet_name=wallets[1]) + self.nodes[0].createwallet(wallet_name=wallets[2]) + w1 = self.nodes[0].get_wallet_rpc(wallets[0]) + w2 = self.nodes[0].get_wallet_rpc(wallets[1]) + w3 = self.nodes[0].get_wallet_rpc(wallets[2]) + w1.walletpassphrase(password, self.rpc_timeout) + w1.sendtoaddress(w2.getnewaddress(), amounts[1]) + w1.sendtoaddress(w3.getnewaddress(), amounts[2]) + + # Mine a block to confirm; adds a block reward (500 DASH) to the default wallet. + self.nodes[0].generate(1) + + self.log.info("Test -getinfo with multiple wallets loaded returns no balance") + assert_equal(set(self.nodes[0].listwallets()), set(wallets)) + assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli().keys() + + 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').send_cli('-rpcwallet={}'.format(wallets[i])) + assert_equal(cli_get_info['balance'], amounts[i]) + + self.log.info("Test -getinfo with multiple wallets and -rpcwallet=non-existing-wallet returns no balance") + assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli('-rpcwallet=does-not-exist').keys() + + self.log.info("Test -getinfo after unloading all wallets except a non-default one returns its balance") + self.nodes[0].unloadwallet(wallets[0]) + self.nodes[0].unloadwallet(wallets[2]) + assert_equal(self.nodes[0].listwallets(), [wallets[1]]) + assert_equal(self.nodes[0].cli('-getinfo').send_cli()['balance'], amounts[1]) + + self.log.info("Test -getinfo -rpcwallet=remaining-non-default-wallet returns its balance") + assert_equal(self.nodes[0].cli('-getinfo').send_cli('-rpcwallet={}'.format(wallets[1]))['balance'], amounts[1]) + + self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balance") + assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli('-rpcwallet={}'.format(wallets[2])).keys() else: self.log.info("*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") + self.nodes[0].generate(1) # maintain block parity with the wallet_compiled conditional branch self.log.info("Test -version with node stopped") self.stop_node(0) @@ -86,7 +127,7 @@ class TestBitcoinCli(BitcoinTestFramework): self.nodes[0].wait_for_cookie_credentials() # ensure cookie file is available to avoid race condition blocks = self.nodes[0].cli('-rpcwait').send_cli('getblockcount') self.nodes[0].wait_for_rpc_connection() - assert_equal(blocks, BLOCKS) + assert_equal(blocks, BLOCKS + 1) if __name__ == '__main__': From 95d462d01d690a3e28332444ea49ce3876999df1 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kittywhiskers@users.noreply.github.com> Date: Mon, 17 Apr 2023 08:28:19 +0000 Subject: [PATCH 05/11] merge bitcoin#18594: display multiwallet balances in -getinfo --- src/bitcoin-cli.cpp | 158 ++++++++++++++--------- src/rpc/request.cpp | 10 +- src/rpc/request.h | 2 +- test/functional/interface_bitcoin_cli.py | 51 +++++--- 4 files changed, 141 insertions(+), 80 deletions(-) diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 4954542002..557b79e369 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -259,7 +259,7 @@ public: UniValue ProcessReply(const UniValue &batch_in) override { UniValue result(UniValue::VOBJ); - std::vector batch = JSONRPCProcessBatchReply(batch_in, batch_in.size()); + 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()) { @@ -314,7 +314,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: @@ -380,14 +380,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"); } } @@ -431,6 +429,65 @@ 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; +} + +/** + * 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); +} + static int CommandLineRPC(int argc, char *argv[]) { std::string strPrint; @@ -487,9 +544,8 @@ 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 { rh.reset(new DefaultRequestHandler()); if (args.size() < 1) { @@ -498,62 +554,46 @@ static int CommandLineRPC(int argc, char *argv[]) method = args[0]; args.erase(args.begin()); // Remove trailing method name from arguments vector } + 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 + UniValue result = find_value(reply, "result"); + const UniValue& error = find_value(reply, "error"); + if (!error.isNull()) { + // Error + strPrint = "error: " + error.write(); + nRet = abs(error["code"].get_int()); + if (error.isObject()) { + const UniValue& errCode = find_value(error, "code"); + const UniValue& errMsg = find_value(error, "message"); + strPrint = errCode.isNull() ? "" : ("error code: " + errCode.getValStr() + "\n"); - // 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); + 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."; } - // Connection succeeded, no need to retry. - break; } - catch (const CConnectionFailed&) { - if (fWait) - UninterruptibleSleep(std::chrono::milliseconds{1000}); - else - throw; + } else { + if (gArgs.IsArgSet("-getinfo") && !gArgs.IsArgSet("-rpcwallet")) { + GetWalletBalances(result); // fetch multiwallet balances and append to result } - } while (fWait); - } - catch (const std::exception& e) { + // Result + if (result.isNull()) { + strPrint = ""; + } else if (result.isStr()) { + strPrint = result.get_str(); + } else { + strPrint = result.write(2); + } + } + } catch (const std::exception& e) { strPrint = std::string("error: ") + e.what(); nRet = EXIT_FAILURE; - } - catch (...) { + } catch (...) { PrintExceptionContinue(std::current_exception(), "CommandLineRPC()"); throw; } diff --git a/src/rpc/request.cpp b/src/rpc/request.cpp index 948aa968ae..6877b7e289 100644 --- a/src/rpc/request.cpp +++ b/src/rpc/request.cpp @@ -130,20 +130,20 @@ void DeleteAuthCookie() } } -std::vector JSONRPCProcessBatchReply(const UniValue &in, size_t num) +std::vector JSONRPCProcessBatchReply(const UniValue& in) { if (!in.isArray()) { throw std::runtime_error("Batch must be an array"); } + const size_t num {in.size()}; std::vector batch(num); - for (size_t i=0; i= num) { - throw std::runtime_error("Batch member id larger than size"); + throw std::runtime_error("Batch member id is larger than batch size"); } batch[id] = rec; } diff --git a/src/rpc/request.h b/src/rpc/request.h index ffce790861..b076403936 100644 --- a/src/rpc/request.h +++ b/src/rpc/request.h @@ -24,7 +24,7 @@ bool GetAuthCookie(std::string *cookie_out); /** Delete RPC authentication cookie from disk */ void DeleteAuthCookie(); /** Parse JSON-RPC batch reply into a vector */ -std::vector JSONRPCProcessBatchReply(const UniValue &in, size_t num); +std::vector JSONRPCProcessBatchReply(const UniValue& in); class JSONRPCRequest { diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 148652e390..262960af2e 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -17,6 +17,8 @@ class TestBitcoinCli(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 + if self.is_wallet_compiled(): + self.requires_wallet = True def skip_test_if_missing_module(self): self.skip_if_no_cli() @@ -67,6 +69,7 @@ class TestBitcoinCli(BitcoinTestFramework): 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() 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']) @@ -76,43 +79,61 @@ class TestBitcoinCli(BitcoinTestFramework): assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) # Setup to test -getinfo and -rpcwallet= with multiple wallets. - wallets = ['', 'Encrypted', 'secret'] - amounts = [Decimal('59.999928'), Decimal(9), Decimal(31)] + wallets = [self.default_wallet_name, 'Encrypted', 'secret'] + amounts = [BALANCE + Decimal('459.9999955'), Decimal(9), Decimal(31)] self.nodes[0].createwallet(wallet_name=wallets[1]) self.nodes[0].createwallet(wallet_name=wallets[2]) w1 = self.nodes[0].get_wallet_rpc(wallets[0]) w2 = self.nodes[0].get_wallet_rpc(wallets[1]) w3 = self.nodes[0].get_wallet_rpc(wallets[2]) w1.walletpassphrase(password, self.rpc_timeout) + w2.encryptwallet(password) w1.sendtoaddress(w2.getnewaddress(), amounts[1]) w1.sendtoaddress(w3.getnewaddress(), amounts[2]) # Mine a block to confirm; adds a block reward (500 DASH) to the default wallet. self.nodes[0].generate(1) - self.log.info("Test -getinfo with multiple wallets loaded returns no balance") - assert_equal(set(self.nodes[0].listwallets()), set(wallets)) - assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli().keys() - 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').send_cli('-rpcwallet={}'.format(wallets[i])) + 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]) - self.log.info("Test -getinfo with multiple wallets and -rpcwallet=non-existing-wallet returns no balance") - assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli('-rpcwallet=does-not-exist').keys() + 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 + + 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)}) + + # 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:])}) self.log.info("Test -getinfo after unloading all wallets except a non-default one returns its balance") - self.nodes[0].unloadwallet(wallets[0]) self.nodes[0].unloadwallet(wallets[2]) assert_equal(self.nodes[0].listwallets(), [wallets[1]]) - assert_equal(self.nodes[0].cli('-getinfo').send_cli()['balance'], amounts[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]) - self.log.info("Test -getinfo -rpcwallet=remaining-non-default-wallet returns its balance") - assert_equal(self.nodes[0].cli('-getinfo').send_cli('-rpcwallet={}'.format(wallets[1]))['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', '-rpcwallet={}'.format(wallets[1])).send_cli() + assert 'balances' not in cli_get_info.keys() + assert_equal(cli_get_info['balance'], amounts[1]) - self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balance") - assert 'balance' not in self.nodes[0].cli('-getinfo').send_cli('-rpcwallet={}'.format(wallets[2])).keys() + self.log.info("Test -getinfo with -rpcwallet=unloaded wallet returns no balances") + cli_get_info = self.nodes[0].cli('-getinfo', '-rpcwallet={}'.format(wallets[2])).send_cli() + assert 'balance' not in cli_get_info_keys + assert 'balances' not in cli_get_info_keys else: self.log.info("*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") self.nodes[0].generate(1) # maintain block parity with the wallet_compiled conditional branch From c89cac578377a2cc4c56896db84fff908c3ccdca Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kittywhiskers@users.noreply.github.com> Date: Mon, 17 Apr 2023 08:27:07 +0000 Subject: [PATCH 06/11] merge bitcoin#19991: Use alternative port for incoming Tor connections --- doc/tor.md | 9 +++--- src/chainparamsbase.cpp | 12 +++++--- src/chainparamsbase.h | 9 ++++-- src/init.cpp | 40 +++++++++++++++++++----- src/net.cpp | 27 ++++++++++++---- src/net.h | 7 ++++- src/torcontrol.cpp | 68 +++++++++++++++++++++++++---------------- src/torcontrol.h | 7 ++++- 8 files changed, 125 insertions(+), 54 deletions(-) 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/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..165d15858a 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -99,6 +99,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 @@ -2875,9 +2880,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 +2976,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 +2998,16 @@ bool CConnman::InitBinds(const std::vector& binds, const std::vectorThreadSafeMessageBox( _("Failed to listen on any port. Use -listen=0 if you want this."), diff --git a/src/net.h b/src/net.h index 773b19b3bd..e4c323f4ac 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; @@ -496,7 +497,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(); diff --git a/src/torcontrol.cpp b/src/torcontrol.cpp index cfcb63d107..38c63e5af2 100644 --- a/src/torcontrol.cpp +++ b/src/torcontrol.cpp @@ -3,14 +3,17 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include #include -#include -#include + +#include +#include +#include #include +#include +#include +#include #include #include -#include #include #include @@ -79,12 +82,12 @@ public: /** * Connect to a Tor control port. - * target is address of the form host:port. + * tor_control_center is address of the form host:port. * connected is the handler that is called when connection is successfully established. * disconnected is a handler that is called when the connection is broken. * Return true on success. */ - bool Connect(const std::string &target, const ConnectionCB& connected, const ConnectionCB& disconnected); + bool Connect(const std::string& tor_control_center, const ConnectionCB& connected, const ConnectionCB& disconnected); /** * Disconnect from Tor control port. @@ -191,16 +194,16 @@ void TorControlConnection::eventcb(struct bufferevent *bev, short what, void *ct } } -bool TorControlConnection::Connect(const std::string &target, const ConnectionCB& _connected, const ConnectionCB& _disconnected) +bool TorControlConnection::Connect(const std::string& tor_control_center, const ConnectionCB& _connected, const ConnectionCB& _disconnected) { if (b_conn) Disconnect(); - // Parse target address:port + // Parse tor_control_center address:port struct sockaddr_storage connect_to_addr; int connect_to_addrlen = sizeof(connect_to_addr); - if (evutil_parse_sockaddr_port(target.c_str(), + if (evutil_parse_sockaddr_port(tor_control_center.c_str(), (struct sockaddr*)&connect_to_addr, &connect_to_addrlen)<0) { - LogPrintf("tor: Error parsing socket address %s\n", target); + LogPrintf("tor: Error parsing socket address %s\n", tor_control_center); return false; } @@ -213,9 +216,9 @@ bool TorControlConnection::Connect(const std::string &target, const ConnectionCB this->connected = _connected; this->disconnected = _disconnected; - // Finally, connect to target + // Finally, connect to tor_control_center if (bufferevent_socket_connect(b_conn, (struct sockaddr*)&connect_to_addr, connect_to_addrlen) < 0) { - LogPrintf("tor: Error connecting to address %s\n", target); + LogPrintf("tor: Error connecting to address %s\n", tor_control_center); return false; } return true; @@ -408,7 +411,7 @@ static bool WriteBinaryFile(const fs::path &filename, const std::string &data) class TorController { public: - TorController(struct event_base* base, const std::string& target); + TorController(struct event_base* base, const std::string& tor_control_center, const CService& target); ~TorController(); /** Get name of file to store private key in */ @@ -418,7 +421,7 @@ public: void Reconnect(); private: struct event_base* base; - std::string target; + const std::string m_tor_control_center; TorControlConnection conn; std::string private_key; std::string service_id; @@ -426,6 +429,7 @@ private: struct event *reconnect_ev; float reconnect_timeout; CService service; + const CService m_target; /** Cookie for SAFECOOKIE auth */ std::vector cookie; /** ClientNonce for SAFECOOKIE auth */ @@ -448,18 +452,19 @@ private: static void reconnect_cb(evutil_socket_t fd, short what, void *arg); }; -TorController::TorController(struct event_base* _base, const std::string& _target): +TorController::TorController(struct event_base* _base, const std::string& tor_control_center, const CService& target): base(_base), - target(_target), conn(base), reconnect(true), reconnect_ev(0), - reconnect_timeout(RECONNECT_TIMEOUT_START) + m_tor_control_center(tor_control_center), conn(base), reconnect(true), reconnect_ev(0), + reconnect_timeout(RECONNECT_TIMEOUT_START), + m_target(target) { reconnect_ev = event_new(base, -1, 0, reconnect_cb, this); if (!reconnect_ev) LogPrintf("tor: Failed to create event for reconnection: out of memory?\n"); // Start connection attempts immediately - if (!conn.Connect(_target, std::bind(&TorController::connected_cb, this, std::placeholders::_1), + if (!conn.Connect(m_tor_control_center, std::bind(&TorController::connected_cb, this, std::placeholders::_1), std::bind(&TorController::disconnected_cb, this, std::placeholders::_1) )) { - LogPrintf("tor: Initiating connection to Tor control port %s failed\n", _target); + LogPrintf("tor: Initiating connection to Tor control port %s failed\n", m_tor_control_center); } // Read service private key if cached std::pair pkf = ReadBinaryFile(GetPrivateKeyFile()); @@ -535,7 +540,7 @@ void TorController::auth_cb(TorControlConnection& _conn, const TorControlReply& } // Request hidden service, redirect port. // Note that the 'virtual' port is always the default port to avoid decloaking nodes using other ports. - _conn.Command(strprintf("ADD_ONION %s Port=%i,127.0.0.1:%i", private_key, Params().GetDefaultPort(), GetListenPort()), + _conn.Command(strprintf("ADD_ONION %s Port=%i,%s", private_key, Params().GetDefaultPort(), m_target.ToStringIPPort()), std::bind(&TorController::add_onion_cb, this, std::placeholders::_1, std::placeholders::_2)); } else { LogPrintf("tor: Authentication failed\n"); @@ -697,7 +702,7 @@ void TorController::disconnected_cb(TorControlConnection& _conn) if (!reconnect) return; - LogPrint(BCLog::TOR, "tor: Not connected to Tor control port %s, trying to reconnect\n", target); + LogPrint(BCLog::TOR, "tor: Not connected to Tor control port %s, trying to reconnect\n", m_tor_control_center); // Single-shot timer for reconnect. Use exponential backoff. struct timeval time = MillisToTimeval(int64_t(reconnect_timeout * 1000.0)); @@ -711,9 +716,9 @@ void TorController::Reconnect() /* Try to reconnect and reestablish if we get booted - for example, Tor * may be restarting. */ - if (!conn.Connect(target, std::bind(&TorController::connected_cb, this, std::placeholders::_1), + if (!conn.Connect(m_tor_control_center, std::bind(&TorController::connected_cb, this, std::placeholders::_1), std::bind(&TorController::disconnected_cb, this, std::placeholders::_1) )) { - LogPrintf("tor: Re-initiating connection to Tor control port %s failed\n", target); + LogPrintf("tor: Re-initiating connection to Tor control port %s failed\n", m_tor_control_center); } } @@ -732,14 +737,14 @@ void TorController::reconnect_cb(evutil_socket_t fd, short what, void *arg) static struct event_base *gBase; static std::thread torControlThread; -static void TorControlThread() +static void TorControlThread(CService onion_service_target) { - TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL)); + TorController ctrl(gBase, gArgs.GetArg("-torcontrol", DEFAULT_TOR_CONTROL), onion_service_target); event_base_dispatch(gBase); } -void StartTorControl() +void StartTorControl(CService onion_service_target) { assert(!gBase); #ifdef WIN32 @@ -753,7 +758,9 @@ void StartTorControl() return; } - torControlThread = std::thread(std::bind(&TraceThread, "torcontrol", &TorControlThread)); + torControlThread = std::thread(&TraceThread>, "torcontrol", [onion_service_target] { + TorControlThread(onion_service_target); + }); } void InterruptTorControl() @@ -774,3 +781,10 @@ void StopTorControl() gBase = nullptr; } } + +CService DefaultOnionServiceTarget() +{ + struct in_addr onion_service_target; + onion_service_target.s_addr = htonl(INADDR_LOOPBACK); + return {onion_service_target, BaseParams().OnionServiceTargetPort()}; +} diff --git a/src/torcontrol.h b/src/torcontrol.h index 4bf3aac2f0..ad5dada39b 100644 --- a/src/torcontrol.h +++ b/src/torcontrol.h @@ -8,12 +8,17 @@ #ifndef BITCOIN_TORCONTROL_H #define BITCOIN_TORCONTROL_H +#include + +class CService; extern const std::string DEFAULT_TOR_CONTROL; static const bool DEFAULT_LISTEN_ONION = true; -void StartTorControl(); +void StartTorControl(CService onion_service_target); void InterruptTorControl(); void StopTorControl(); +CService DefaultOnionServiceTarget(); + #endif /* BITCOIN_TORCONTROL_H */ From 1fab09d8faf9a2a97ca4aef1dce5a12a49a68820 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kittywhiskers@users.noreply.github.com> Date: Sun, 16 Apr 2023 19:53:03 +0000 Subject: [PATCH 07/11] merge bitcoin#19133: add bitcoin-cli -generate command --- src/Makefile.am | 1 + src/bitcoin-cli.cpp | 136 +++++++++++++++++------ src/rpc/client.cpp | 2 +- src/rpc/mining.cpp | 18 ++- src/rpc/mining.h | 11 ++ test/functional/interface_bitcoin_cli.py | 102 ++++++++++++++++- 6 files changed, 222 insertions(+), 48 deletions(-) create mode 100644 src/rpc/mining.h 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 557b79e369..8aab897add 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,6 +54,7 @@ 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("-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); @@ -294,6 +299,28 @@ public: } }; +/** 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: @@ -464,6 +491,34 @@ static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& str 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`. @@ -488,6 +543,34 @@ static void GetWalletBalances(UniValue& result) 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; @@ -546,6 +629,15 @@ static int CommandLineRPC(int argc, char *argv[]) std::string method; if (gArgs.IsArgSet("-getinfo")) { rh.reset(new GetinfoRequestHandler()); + } 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) { @@ -554,40 +646,22 @@ static int CommandLineRPC(int argc, char *argv[]) method = args[0]; args.erase(args.begin()); // Remove trailing method name from arguments vector } - std::optional wallet_name{}; - if (gArgs.IsArgSet("-rpcwallet")) wallet_name = gArgs.GetArg("-rpcwallet", ""); - const UniValue reply = ConnectAndCallRPC(rh.get(), method, args, wallet_name); + 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); - // Parse reply - UniValue result = find_value(reply, "result"); - const UniValue& error = find_value(reply, "error"); - if (!error.isNull()) { - // Error - strPrint = "error: " + error.write(); - nRet = abs(error["code"].get_int()); - if (error.isObject()) { - const UniValue& errCode = find_value(error, "code"); - const UniValue& errMsg = find_value(error, "message"); - strPrint = errCode.isNull() ? "" : ("error code: " + errCode.getValStr() + "\n"); - - if (errMsg.isStr()) { - strPrint += ("error message:\n" + errMsg.get_str()); + // 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 } - if (errCode.isNum() && errCode.get_int() == RPC_WALLET_NOT_SPECIFIED) { - strPrint += "\nTry adding \"-rpcwallet=\" option to dash-cli command line."; - } - } - } else { - if (gArgs.IsArgSet("-getinfo") && !gArgs.IsArgSet("-rpcwallet")) { - GetWalletBalances(result); // fetch multiwallet balances and append to result - } - // Result - if (result.isNull()) { - strPrint = ""; - } else if (result.isStr()) { - strPrint = result.get_str(); + ParseResult(result, strPrint); } else { - strPrint = result.write(2); + ParseError(error, strPrint, nRet); } } } catch (const std::exception& e) { 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