diff --git a/src/addrman.cpp b/src/addrman.cpp index 7959ec2b74ca..e1c26d992bb0 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -808,7 +808,7 @@ int AddrManImpl::GetEntry(bool use_tried, size_t bucket, size_t position) const return -1; } -std::vector AddrManImpl::GetAddr_(size_t max_addresses, size_t max_pct, std::optional network) const +std::vector AddrManImpl::GetAddr_(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered) const { AssertLockHeld(cs); @@ -838,7 +838,7 @@ std::vector AddrManImpl::GetAddr_(size_t max_addresses, size_t max_pct if (network != std::nullopt && ai.GetNetClass() != network) continue; // Filter for quality - if (ai.IsTerrible(now)) continue; + if (ai.IsTerrible(now) && filtered) continue; addresses.push_back(ai); } @@ -1209,11 +1209,11 @@ std::pair AddrManImpl::Select(bool new_only, std::optiona return addrRet; } -std::vector AddrManImpl::GetAddr(size_t max_addresses, size_t max_pct, std::optional network) const +std::vector AddrManImpl::GetAddr(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered) const { LOCK(cs); Check(); - const auto addresses = GetAddr_(max_addresses, max_pct, network); + const auto addresses = GetAddr_(max_addresses, max_pct, network, filtered); Check(); return addresses; } @@ -1315,9 +1315,9 @@ std::pair AddrMan::Select(bool new_only, std::optionalSelect(new_only, network); } -std::vector AddrMan::GetAddr(size_t max_addresses, size_t max_pct, std::optional network) const +std::vector AddrMan::GetAddr(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered) const { - return m_impl->GetAddr(max_addresses, max_pct, network); + return m_impl->GetAddr(max_addresses, max_pct, network, filtered); } void AddrMan::Connected(const CService& addr, NodeSeconds time) diff --git a/src/addrman.h b/src/addrman.h index c31fb321f82c..61693baf4fc8 100644 --- a/src/addrman.h +++ b/src/addrman.h @@ -177,10 +177,11 @@ class AddrMan * @param[in] max_addresses Maximum number of addresses to return (0 = all). * @param[in] max_pct Maximum percentage of addresses to return (0 = all). * @param[in] network Select only addresses of this network (nullopt = all). + * @param[in] filtered Select only addresses that are considered good quality (false = all). * * @return A vector of randomly selected addresses from vRandom. */ - std::vector GetAddr(size_t max_addresses, size_t max_pct, std::optional network) const; + std::vector GetAddr(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered = true) const; /** We have successfully connected to this peer. Calling this function * updates the CAddress's nTime, which is used in our IsTerrible() diff --git a/src/addrman_impl.h b/src/addrman_impl.h index 08c597e66765..890685260eeb 100644 --- a/src/addrman_impl.h +++ b/src/addrman_impl.h @@ -130,7 +130,7 @@ class AddrManImpl std::pair Select(bool new_only, std::optional network) const EXCLUSIVE_LOCKS_REQUIRED(!cs); - std::vector GetAddr(size_t max_addresses, size_t max_pct, std::optional network) const + std::vector GetAddr(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered = true) const EXCLUSIVE_LOCKS_REQUIRED(!cs); void Connected(const CService& addr, NodeSeconds time) @@ -262,7 +262,7 @@ class AddrManImpl * */ int GetEntry(bool use_tried, size_t bucket, size_t position) const EXCLUSIVE_LOCKS_REQUIRED(cs); - std::vector GetAddr_(size_t max_addresses, size_t max_pct, std::optional network) const EXCLUSIVE_LOCKS_REQUIRED(cs); + std::vector GetAddr_(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered = true) const EXCLUSIVE_LOCKS_REQUIRED(cs); void Connected_(const CService& addr, NodeSeconds time) EXCLUSIVE_LOCKS_REQUIRED(cs); diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 46cb1041c23e..f09ca024fb6b 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include diff --git a/src/net.cpp b/src/net.cpp index f62ebb16820d..37cf52b64895 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -4089,6 +4089,12 @@ bool CConnman::Start(CDeterministicMNManager& dmnman, CMasternodeMetaMan& mn_met // Dump network addresses scheduler.scheduleEvery([this] { DumpAddresses(); }, DUMP_PEERS_INTERVAL); + // Run the ASMap Health check once and then schedule it to run every 24h. + if (m_netgroupman.UsingASMap()) { + ASMapHealthCheck(); + scheduler.scheduleEvery([this] { ASMapHealthCheck(); }, ASMAP_HEALTH_CHECK_INTERVAL); + } + return true; } @@ -4223,9 +4229,9 @@ CConnman::~CConnman() Stop(); } -std::vector CConnman::GetAddresses(size_t max_addresses, size_t max_pct, std::optional network) const +std::vector CConnman::GetAddresses(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered) const { - std::vector addresses = addrman.GetAddr(max_addresses, max_pct, network); + std::vector addresses = addrman.GetAddr(max_addresses, max_pct, network, filtered); if (m_banman) { addresses.erase(std::remove_if(addresses.begin(), addresses.end(), [this](const CAddress& addr){return m_banman->IsDiscouraged(addr) || m_banman->IsBanned(addr);}), @@ -4945,6 +4951,19 @@ void CConnman::PerformReconnections() } } +void CConnman::ASMapHealthCheck() +{ + const std::vector v4_addrs{GetAddresses(/*max_addresses=*/ 0, /*max_pct=*/ 0, Network::NET_IPV4, /*filtered=*/ false)}; + const std::vector v6_addrs{GetAddresses(/*max_addresses=*/ 0, /*max_pct=*/ 0, Network::NET_IPV6, /*filtered=*/ false)}; + std::vector clearnet_addrs; + clearnet_addrs.reserve(v4_addrs.size() + v6_addrs.size()); + std::transform(v4_addrs.begin(), v4_addrs.end(), std::back_inserter(clearnet_addrs), + [](const CAddress& addr) { return static_cast(addr); }); + std::transform(v6_addrs.begin(), v6_addrs.end(), std::back_inserter(clearnet_addrs), + [](const CAddress& addr) { return static_cast(addr); }); + m_netgroupman.ASMapHealthCheck(clearnet_addrs); +} + // Dump binary message to file, with timestamp. static void CaptureMessageToFile(const CAddress& addr, const std::string& msg_type, diff --git a/src/net.h b/src/net.h index 1da1093f187b..671a6404ae17 100644 --- a/src/net.h +++ b/src/net.h @@ -111,6 +111,8 @@ static const bool DEFAULT_BLOCKSONLY = false; static const int64_t DEFAULT_PEER_CONNECT_TIMEOUT = 60; /** Number of file descriptors required for message capture **/ static const int NUM_FDS_MESSAGE_CAPTURE = 1; +/** Interval for ASMap Health Check **/ +static constexpr std::chrono::hours ASMAP_HEALTH_CHECK_INTERVAL{24}; static constexpr bool DEFAULT_FORCEDNSSEED{false}; static constexpr bool DEFAULT_DNSSEED{true}; @@ -1295,6 +1297,7 @@ friend class CNode; void OpenMasternodeConnection(const CAddress& addrConnect, bool use_v2transport, MasternodeProbeConn probe = MasternodeProbeConn::IsConnection) EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_unused_i2p_sessions_mutex, !mutexMsgProc, !cs_mapSocketToNode); bool CheckIncomingNonce(uint64_t nonce) const EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + void ASMapHealthCheck(); // alias for thread safety annotations only, not defined SharedMutex& GetNodesMutex() const LOCK_RETURNED(m_nodes_mutex); @@ -1405,8 +1408,9 @@ friend class CNode; * @param[in] max_addresses Maximum number of addresses to return (0 = all). * @param[in] max_pct Maximum percentage of addresses to return (0 = all). * @param[in] network Select only addresses of this network (nullopt = all). + * @param[in] filtered Select only addresses that are considered high quality (false = all). */ - std::vector GetAddresses(size_t max_addresses, size_t max_pct, std::optional network) const; + std::vector GetAddresses(size_t max_addresses, size_t max_pct, std::optional network, const bool filtered = true) const; /** * Cache is used to minimize topology leaks, so it should diff --git a/src/netgroup.cpp b/src/netgroup.cpp index 491017bc15b1..ff04a9a9f994 100644 --- a/src/netgroup.cpp +++ b/src/netgroup.cpp @@ -5,6 +5,7 @@ #include #include +#include #include uint256 NetGroupManager::GetAsmapChecksum() const @@ -109,3 +110,23 @@ uint32_t NetGroupManager::GetMappedAS(const CNetAddr& address) const uint32_t mapped_as = Interpret(m_asmap, ip_bits); return mapped_as; } + +void NetGroupManager::ASMapHealthCheck(const std::vector& clearnet_addrs) const { + std::set clearnet_asns{}; + int unmapped_count{0}; + + for (const auto& addr : clearnet_addrs) { + uint32_t asn = GetMappedAS(addr); + if (asn == 0) { + ++unmapped_count; + continue; + } + clearnet_asns.insert(asn); + } + + LogPrintf("ASMap Health Check: %i clearnet peers are mapped to %i ASNs with %i peers being unmapped\n", clearnet_addrs.size(), clearnet_asns.size(), unmapped_count); +} + +bool NetGroupManager::UsingASMap() const { + return m_asmap.size() > 0; +} diff --git a/src/netgroup.h b/src/netgroup.h index 2dd63ec66b05..5aa6ef774253 100644 --- a/src/netgroup.h +++ b/src/netgroup.h @@ -41,6 +41,16 @@ class NetGroupManager { */ uint32_t GetMappedAS(const CNetAddr& address) const; + /** + * Analyze and log current health of ASMap based buckets. + */ + void ASMapHealthCheck(const std::vector& clearnet_addrs) const; + + /** + * Indicates whether ASMap is being used for clearnet bucketing. + */ + bool UsingASMap() const; + private: /** Compressed IP->ASN mapping, loaded from a file when a node starts. * diff --git a/src/rest.cpp b/src/rest.cpp index 68f5fc79425d..5e015a56cdd8 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index adaf7eee581a..ac9cdb79cff9 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index 3ed094b36029..e67d93e74260 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -95,6 +96,18 @@ static RPCHelpMan ping() }; } +/** Returns, given services flags, a list of humanly readable (known) network services */ +static UniValue GetServicesNames(ServiceFlags services) +{ + UniValue servicesNames(UniValue::VARR); + + for (const auto& flag : serviceFlagsToStr(services)) { + servicesNames.push_back(flag); + } + + return servicesNames; +} + static RPCHelpMan getpeerinfo() { return RPCHelpMan{ diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 5db203c01832..1bb011243696 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -198,8 +198,9 @@ static std::vector CreateTxDoc() }, }, }, - {"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs (key-value pairs), where none of the keys are duplicated.\n" - "That is, each address can only appear once and there can only be one 'data' object.\n" + {"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs specified as key-value pairs.\n" + "Each key may only appear once, i.e. there can only be one 'data' output, and no address may be duplicated.\n" + "At least one output of either type must be specified.\n" "For compatibility reasons, a dictionary, which holds the key-value pairs directly, is also\n" " accepted as second parameter.", { diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 67d012ab2bcd..e8e6b5571bc1 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -990,17 +990,6 @@ UniValue JSONRPCTransactionError(TransactionError terr, const std::string& err_s } } -UniValue GetServicesNames(ServiceFlags services) -{ - UniValue servicesNames(UniValue::VARR); - - for (const auto& flag : serviceFlagsToStr(services)) { - servicesNames.push_back(flag); - } - - return servicesNames; -} - std::vector EvalDescriptorStringOrObject(const UniValue& scanobject, FlatSigningProvider& provider) { std::string desc_str; diff --git a/src/rpc/util.h b/src/rpc/util.h index 6a62ca00e3e2..47acd28f0936 100644 --- a/src/rpc/util.h +++ b/src/rpc/util.h @@ -6,7 +6,6 @@ #define BITCOIN_RPC_UTIL_H #include -#include #include #include #include @@ -44,7 +43,6 @@ extern const std::string EXAMPLE_ADDRESS[2]; class FillableSigningProvider; class FillableSigningProvider; -class CPubKey; class CScript; struct Sections; @@ -122,8 +120,6 @@ UniValue DescribeAddress(const CTxDestination& dest); //! Parse a confirm target option and raise an RPC error if it is invalid. unsigned int ParseConfirmTarget(const UniValue& value, unsigned int max_target); -/** Returns, given services flags, a list of humanly readable (known) network services */ -UniValue GetServicesNames(ServiceFlags services); //! Parse a JSON range specified as int64, or [int64, int64] std::pair ParseDescriptorRange(const UniValue& value); diff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp index 8022dd8c2643..2efb321266bb 100644 --- a/src/test/addrman_tests.cpp +++ b/src/test/addrman_tests.cpp @@ -428,6 +428,24 @@ BOOST_AUTO_TEST_CASE(addrman_getaddr) BOOST_CHECK_EQUAL(addrman->Size(), 2006U); } +BOOST_AUTO_TEST_CASE(getaddr_unfiltered) +{ + auto addrman = std::make_unique(EMPTY_NETGROUPMAN, DETERMINISTIC, GetCheckRatio(m_node)); + + // Set time on this addr so isTerrible = false + CAddress addr1 = CAddress(ResolveService("250.250.2.1", 8333), NODE_NONE); + addr1.nTime = Now(); + // Not setting time so this addr should be isTerrible = true + CAddress addr2 = CAddress(ResolveService("250.251.2.2", 9999), NODE_NONE); + + CNetAddr source = ResolveIP("250.1.2.1"); + BOOST_CHECK(addrman->Add({addr1, addr2}, source)); + + // Filtered GetAddr should only return addr1 + BOOST_CHECK_EQUAL(addrman->GetAddr(/*max_addresses=*/0, /*max_pct=*/0, /*network=*/std::nullopt).size(), 1U); + // Unfiltered GetAddr should return addr1 and addr2 + BOOST_CHECK_EQUAL(addrman->GetAddr(/*max_addresses=*/0, /*max_pct=*/0, /*network=*/std::nullopt, /*filtered=*/false).size(), 2U); +} BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket_legacy) { diff --git a/src/test/fuzz/addrman.cpp b/src/test/fuzz/addrman.cpp index 1a79febb8a70..4847ce01581e 100644 --- a/src/test/fuzz/addrman.cpp +++ b/src/test/fuzz/addrman.cpp @@ -283,7 +283,8 @@ FUZZ_TARGET(addrman, .init = initialize_addrman) (void)const_addr_man.GetAddr( /* max_addresses */ fuzzed_data_provider.ConsumeIntegralInRange(0, 4096), /* max_pct */ fuzzed_data_provider.ConsumeIntegralInRange(0, 4096), - /* network */ std::nullopt); + /* network */ std::nullopt, + /* filtered =*/ fuzzed_data_provider.ConsumeBool()); (void)const_addr_man.Select(fuzzed_data_provider.ConsumeBool()); (void)const_addr_man.Size(); CDataStream data_stream(SER_NETWORK, PROTOCOL_VERSION); diff --git a/src/test/fuzz/connman.cpp b/src/test/fuzz/connman.cpp index 54c0edbbd6ea..2ba8773ab1ea 100644 --- a/src/test/fuzz/connman.cpp +++ b/src/test/fuzz/connman.cpp @@ -94,7 +94,8 @@ FUZZ_TARGET(connman, .init = initialize_connman) (void)connman.GetAddresses( /*max_addresses=*/fuzzed_data_provider.ConsumeIntegral(), /*max_pct=*/fuzzed_data_provider.ConsumeIntegral(), - /*network=*/std::nullopt); + /*network=*/std::nullopt, + /*filtered=*/fuzzed_data_provider.ConsumeBool()); }, [&] { (void)connman.GetAddresses( diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index 17a592c8302f..560d9509ac57 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -803,8 +803,9 @@ RPCHelpMan send() "\nEXPERIMENTAL warning: this call may be changed in future releases.\n" "\nSend a transaction.\n", { - {"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs (key-value pairs), where none of the keys are duplicated.\n" - "That is, each address can only appear once and there can only be one 'data' object.\n" + {"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs specified as key-value pairs.\n" + "Each key may only appear once, i.e. there can only be one 'data' output, and no address may be duplicated.\n" + "At least one output of either type must be specified.\n" "For convenience, a dictionary, which holds the key-value pairs directly, is also accepted.", { {"", RPCArg::Type::OBJ_USER_KEYS, RPCArg::Optional::OMITTED, "", @@ -1097,8 +1098,9 @@ RPCHelpMan walletcreatefundedpsbt() }, }, }, - {"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs (key-value pairs), where none of the keys are duplicated.\n" - "That is, each address can only appear once and there can only be one 'data' object.\n" + {"outputs", RPCArg::Type::ARR, RPCArg::Optional::NO, "The outputs specified as key-value pairs.\n" + "Each key may only appear once, i.e. there can only be one 'data' output, and no address may be duplicated.\n" + "At least one output of either type must be specified.\n" "For compatibility reasons, a dictionary, which holds the key-value pairs directly, is also\n" "accepted as second parameter.", { diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index 7fc43fa5ef2a..163ba6449b03 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -192,6 +192,7 @@ static RPCHelpMan getwalletinfo() }}, {RPCResult::Type::BOOL, "descriptors", "whether this wallet uses descriptors for scriptPubKey management"}, {RPCResult::Type::BOOL, "external_signer", "whether this wallet is configured to use an external signer such as a hardware wallet"}, + {RPCResult::Type::BOOL, "blank", "Whether this wallet intentionally does not contain any keys, scripts, or descriptors"}, RESULT_LAST_PROCESSED_BLOCK, }, }, @@ -271,6 +272,7 @@ static RPCHelpMan getwalletinfo() } obj.pushKV("descriptors", pwallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)); obj.pushKV("external_signer", pwallet->IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)); + obj.pushKV("blank", pwallet->IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET)); AppendLastProcessedBlock(obj, *pwallet); return obj; diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 9615bbf7e0f3..01a8a9831b01 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -899,12 +899,12 @@ bool LegacyScriptPubKeyMan::AddKeyPubKeyWithDB(WalletBatch& batch, const CKey& s RemoveWatchOnly(script); } + m_storage.UnsetBlankWalletFlag(batch); if (!m_storage.HasEncryptionKeys()) { return batch.WriteKey(pubkey, secret.GetPrivKey(), mapKeyMetadata[pubkey.GetID()]); } - m_storage.UnsetBlankWalletFlag(batch); return true; } diff --git a/src/wallet/walletutil.h b/src/wallet/walletutil.h index 42a22cd78cdf..3f6d07590148 100644 --- a/src/wallet/walletutil.h +++ b/src/wallet/walletutil.h @@ -47,10 +47,18 @@ enum WalletFlags : uint64_t { //! Flag set when a wallet contains no HD seed and no private keys, scripts, //! addresses, and other watch only things, and is therefore "blank." //! - //! The only function this flag serves is to distinguish a blank wallet from + //! The main function this flag serves is to distinguish a blank wallet from //! a newly created wallet when the wallet database is loaded, to avoid //! initialization that should only happen on first run. //! + //! A secondary function of this flag, which applies to descriptor wallets + //! only, is to serve as an ongoing indication that descriptors in the + //! wallet should be created manually, and that the wallet should not + //! generate automatically generate new descriptors if it is later + //! encrypted. To support this behavior, descriptor wallets unlike legacy + //! wallets do not automatically unset the BLANK flag when things are + //! imported. + //! //! This flag is also a mandatory flag to prevent previous versions of //! bitcoin from opening the wallet, thinking it was newly created, and //! then improperly reinitializing it. diff --git a/test/functional/example_test.py b/test/functional/example_test.py index 17ca92d00776..288959c4d3c4 100755 --- a/test/functional/example_test.py +++ b/test/functional/example_test.py @@ -79,6 +79,9 @@ class ExampleTest(BitcoinTestFramework): # Override the set_test_params(), skip_test_if_missing_module(), add_options(), setup_chain(), setup_network() # and setup_nodes() methods to customize the test setup as required. + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): """Override test parameters for your individual test. diff --git a/test/functional/feature_asmap.py b/test/functional/feature_asmap.py index a357ee8cbbcd..dfcc0e70b357 100755 --- a/test/functional/feature_asmap.py +++ b/test/functional/feature_asmap.py @@ -111,6 +111,14 @@ def test_empty_asmap(self): self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg) os.remove(self.default_asmap) + def test_asmap_health_check(self): + self.log.info('Test bitcoind -asmap logs ASMap Health Check with basic stats') + shutil.copyfile(self.asmap_raw, self.default_asmap) + msg = "ASMap Health Check: 2 clearnet peers are mapped to 1 ASNs with 0 peers being unmapped" + with self.node.assert_debug_log(expected_msgs=[msg]): + self.start_node(0, extra_args=['-asmap']) + os.remove(self.default_asmap) + def run_test(self): self.node = self.nodes[0] self.datadir = os.path.join(self.node.datadir, self.chain) @@ -124,6 +132,7 @@ def run_test(self): self.test_asmap_interaction_with_addrman_containing_entries() self.test_default_asmap_with_missing_file() self.test_empty_asmap() + self.test_asmap_health_check() if __name__ == '__main__': diff --git a/test/functional/feature_backwards_compatibility.py b/test/functional/feature_backwards_compatibility.py index d954278daf3d..de1e330a1c83 100755 --- a/test/functional/feature_backwards_compatibility.py +++ b/test/functional/feature_backwards_compatibility.py @@ -31,6 +31,9 @@ class BackwardsCompatibilityTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 8 diff --git a/test/functional/feature_bip68_sequence.py b/test/functional/feature_bip68_sequence.py index c7ca1ec190f9..ed899bfa88ca 100755 --- a/test/functional/feature_bip68_sequence.py +++ b/test/functional/feature_bip68_sequence.py @@ -41,6 +41,9 @@ NOT_FINAL_ERROR = "non-BIP68-final" class BIP68Test(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 self.extra_args = [ diff --git a/test/functional/feature_config_args.py b/test/functional/feature_config_args.py index 2e8e58a6027f..9bdadfe794c1 100755 --- a/test/functional/feature_config_args.py +++ b/test/functional/feature_config_args.py @@ -12,6 +12,9 @@ class ConfArgsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/feature_filelock.py b/test/functional/feature_filelock.py index af5509a77db6..31f5225ae19c 100755 --- a/test/functional/feature_filelock.py +++ b/test/functional/feature_filelock.py @@ -13,6 +13,9 @@ class FilelockTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py index b4227e65fd54..ce12319933de 100755 --- a/test/functional/feature_init.py +++ b/test/functional/feature_init.py @@ -19,6 +19,9 @@ class InitStressTest(BitcoinTestFramework): subsequent starts. """ + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = False self.num_nodes = 1 diff --git a/test/functional/feature_notifications.py b/test/functional/feature_notifications.py index 71dbf3cdf63a..244cc6a36a51 100755 --- a/test/functional/feature_notifications.py +++ b/test/functional/feature_notifications.py @@ -26,6 +26,8 @@ def notify_outputname(walletname, txid): class NotificationsTest(DashTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) def set_test_params(self): self.set_dash_test_params(6, 4) diff --git a/test/functional/feature_pruning.py b/test/functional/feature_pruning.py index a58ea8c514d9..fb68cfdbe7b6 100755 --- a/test/functional/feature_pruning.py +++ b/test/functional/feature_pruning.py @@ -88,6 +88,9 @@ def calc_usage(blockdir): return sum(os.path.getsize(blockdir + f) for f in os.listdir(blockdir) if os.path.isfile(os.path.join(blockdir, f))) / (1024. * 1024.) class PruneTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 6 diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index a81f6e25f11b..e44674dbc3c2 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -66,11 +66,12 @@ def cli_get_info_string_to_dict(cli_get_info_string): class TestBitcoinCli(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 - if self.is_specified_wallet_compiled(): - self.requires_wallet = True def skip_test_if_missing_module(self): self.skip_if_no_cli() @@ -123,6 +124,7 @@ def run_test(self): self.log.info("Test -getinfo returns expected network and blockchain info") if self.is_specified_wallet_compiled(): + self.import_deterministic_coinbase_privkeys() self.nodes[0].encryptwallet(password) cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) diff --git a/test/functional/interface_usdt_coinselection.py b/test/functional/interface_usdt_coinselection.py index 8148e651a2e7..cb1fe12a409b 100755 --- a/test/functional/interface_usdt_coinselection.py +++ b/test/functional/interface_usdt_coinselection.py @@ -97,6 +97,9 @@ class CoinSelectionTracepointTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True diff --git a/test/functional/mempool_compatibility.py b/test/functional/mempool_compatibility.py index b576385651e1..7a7a322992e9 100755 --- a/test/functional/mempool_compatibility.py +++ b/test/functional/mempool_compatibility.py @@ -18,6 +18,9 @@ class MempoolCompatibilityTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 diff --git a/test/functional/mempool_packages.py b/test/functional/mempool_packages.py index b922a7678bf4..33651108461e 100755 --- a/test/functional/mempool_packages.py +++ b/test/functional/mempool_packages.py @@ -28,6 +28,9 @@ class MempoolPackagesTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 self.extra_args = [ diff --git a/test/functional/mempool_persist.py b/test/functional/mempool_persist.py index 0bdc8c0a7636..57d71abfe58d 100755 --- a/test/functional/mempool_persist.py +++ b/test/functional/mempool_persist.py @@ -48,6 +48,9 @@ class MempoolPersistTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + def set_test_params(self): self.num_nodes = 3 self.extra_args = [[], ["-persistmempool=0"], []] diff --git a/test/functional/mempool_unbroadcast.py b/test/functional/mempool_unbroadcast.py index 0a3eb0492290..b8d7e9e0f5e3 100755 --- a/test/functional/mempool_unbroadcast.py +++ b/test/functional/mempool_unbroadcast.py @@ -15,10 +15,11 @@ MAX_INITIAL_BROADCAST_DELAY = 15 * 60 # 15 minutes in seconds class MempoolUnbroadcastTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 - if self.is_wallet_compiled(): - self.requires_wallet = True def run_test(self): self.wallet = MiniWallet(self.nodes[0]) @@ -35,6 +36,7 @@ def test_broadcast(self): self.log.info("Generate transactions that only node 0 knows about") if self.is_wallet_compiled(): + self.import_deterministic_coinbase_privkeys() # generate a wallet txn addr = node.getnewaddress() wallet_tx_hsh = node.sendtoaddress(addr, 0.0001) diff --git a/test/functional/rpc_createmultisig.py b/test/functional/rpc_createmultisig.py index 8b7b06944734..509c8721a2d3 100755 --- a/test/functional/rpc_createmultisig.py +++ b/test/functional/rpc_createmultisig.py @@ -23,12 +23,13 @@ ) class RpcCreateMultiSigTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 self.supports_cli = False - if self.is_bdb_compiled(): - self.requires_wallet = True def get_keys(self): self.pub = [] @@ -49,6 +50,7 @@ def run_test(self): self.wallet = MiniWallet(test_node=node0) if self.is_bdb_compiled(): + self.import_deterministic_coinbase_privkeys() self.check_addmultisigaddress_errors() self.log.info('Generating blocks ...') diff --git a/test/functional/rpc_fundrawtransaction.py b/test/functional/rpc_fundrawtransaction.py index 1326ba893be2..8040ef64ee65 100755 --- a/test/functional/rpc_fundrawtransaction.py +++ b/test/functional/rpc_fundrawtransaction.py @@ -32,6 +32,7 @@ def get_unspent(listunspent, amount): raise AssertionError('Could not find unspent with amount={}'.format(amount)) class RawTransactionsTest(BitcoinTestFramework): + def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True @@ -45,6 +46,7 @@ def skip_test_if_missing_module(self): def add_options(self, parser): parser.add_argument("--nohd", dest="nohd", default=False, action="store_true", help="Test with -nohd enabled") + self.add_wallet_options(parser) def setup_network(self): self.setup_nodes() diff --git a/test/functional/rpc_help.py b/test/functional/rpc_help.py index 30573822f260..b362cf068c3e 100755 --- a/test/functional/rpc_help.py +++ b/test/functional/rpc_help.py @@ -43,6 +43,9 @@ def process_mapping(fname): class HelpRpcTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 self.supports_cli = False diff --git a/test/functional/rpc_invalid_address_message.py b/test/functional/rpc_invalid_address_message.py index 18d85d677030..2f6df8570c94 100755 --- a/test/functional/rpc_invalid_address_message.py +++ b/test/functional/rpc_invalid_address_message.py @@ -18,6 +18,9 @@ INVALID_ADDRESS = 'asfah14i8fajz0123f' class InvalidAddressErrorMessageTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py index ea6684ede75c..02fc6f1da642 100755 --- a/test/functional/rpc_psbt.py +++ b/test/functional/rpc_psbt.py @@ -39,8 +39,9 @@ import json import os -# Create one-input, one-output, no-fee transaction: class PSBTTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) def set_test_params(self): self.num_nodes = 3 diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 7854db8a09fb..ad0c55ba82e3 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -53,6 +53,9 @@ def items(self): class RawTransactionsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, descriptors=False) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 @@ -64,8 +67,6 @@ def set_test_params(self): # whitelist all peers to speed up tx relay / mempool sync for args in self.extra_args: args.append("-whitelist=noban@127.0.0.1") - self.requires_wallet = self.is_specified_wallet_compiled() - self.supports_cli = False def setup_network(self): @@ -83,7 +84,8 @@ def run_test(self): self.sendrawtransaction_tests() self.sendrawtransaction_testmempoolaccept_tests() self.transaction_version_number_tests() - if self.requires_wallet and not self.options.descriptors: + if self.is_specified_wallet_compiled() and not self.options.descriptors: + self.import_deterministic_coinbase_privkeys() self.raw_multisig_transaction_legacy_tests() def getrawtransaction_tests(self): diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 67ceeb167625..e3381c72f509 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -141,7 +141,7 @@ def __init__(self): self.wallet_names = None # By default the wallet is not required. Set to true by skip_if_no_wallet(). # When False, we ignore wallet_names regardless of what it is. - self.requires_wallet = False + self._requires_wallet = False # Disable ThreadOpenConnections by default, so that adding entries to # addrman will not result in automatic connections to them. self.disable_autoconnect = True @@ -230,12 +230,6 @@ def parse_args(self): parser.add_argument("--v1transport", dest="v1transport", default=False, action="store_true", help="Explicitly use v1 transport (can be used to overwrite global --v2transport option)") - group = parser.add_mutually_exclusive_group() - group.add_argument("--descriptors", action='store_const', const=True, - help="Run test using a descriptor wallet", dest='descriptors') - group.add_argument("--legacy-wallet", action='store_const', const=False, - help="Run test using legacy wallets", dest='descriptors') - self.add_options(parser) self.options = parser.parse_args() if self.options.timeout_factor == 0: @@ -254,7 +248,13 @@ def parse_args(self): # source: https://stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter/56349168#56349168 parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") - if self.options.descriptors is None: + if "descriptors" not in self.options: + # Wallet is not required by the test at all and the value of self.options.descriptors won't matter. + # It still needs to exist and be None in order for tests to work however. + # So set it to None to force -disablewallet, because the wallet is not needed. + self.options.descriptors = None + elif self.options.descriptors is None: + # Some wallet is either required or optionally used by the test. # Prefer BDB unless it isn't available if self.is_bdb_compiled(): self.options.descriptors = False @@ -263,6 +263,7 @@ def parse_args(self): else: # If neither are compiled, tests requiring a wallet will be skipped and the value of self.options.descriptors won't matter # It still needs to exist and be None in order for tests to work however. + # So set it to None, which will also set -disablewallet. self.options.descriptors = None PortSeed.n = self.options.port_seed @@ -461,7 +462,7 @@ def setup_nodes(self): """ NOTE! If this method is updated - backport changes to DashTestFramework.setup_nodes""" self.add_nodes(self.num_nodes, self.extra_args) self.start_nodes() - if self.requires_wallet: + if self._requires_wallet: self.import_deterministic_coinbase_privkeys() if not self.setup_clean_chain: for n in self.nodes: @@ -497,6 +498,19 @@ def run_test(self): # Public helper methods. These can be accessed by the subclass test scripts. + def add_wallet_options(self, parser, *, descriptors=True, legacy=True): + group = parser.add_mutually_exclusive_group() + kwargs = {} + if descriptors + legacy == 1: + # If only one type can be chosen, set it as default + kwargs["default"] = descriptors + if descriptors: + group.add_argument("--descriptors", action='store_const', const=True, **kwargs, + help="Run test using a descriptor wallet", dest='descriptors') + if legacy: + group.add_argument("--legacy-wallet", action='store_const', const=False, **kwargs, + help="Run test using legacy wallets", dest='descriptors') + def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, binary=None, binary_cli=None, versions=None): """Instantiate TestNode objects. @@ -1049,7 +1063,7 @@ def skip_if_no_bitcoind_zmq(self): def skip_if_no_wallet(self): """Skip the running test if wallet has not been compiled.""" - self.requires_wallet = True + self._requires_wallet = True if not self.is_wallet_compiled(): raise SkipTest("wallet has not been compiled.") if self.options.descriptors: diff --git a/test/functional/test_framework/test_node.py b/test/functional/test_framework/test_node.py index 2c1497cc3cd5..68d0f1722d4f 100755 --- a/test/functional/test_framework/test_node.py +++ b/test/functional/test_framework/test_node.py @@ -114,8 +114,10 @@ def __init__(self, i, datadir, extra_args_from_options, *, chain, rpchost, timew if self.mocktime != 0: self.args.append(f"-mocktime={mocktime}") - # Use valgrind, expect for previous release binaries - if use_valgrind and version is None: + if self.descriptors is None: + self.args.append("-disablewallet") + + if use_valgrind: default_suppressions_file = os.path.join( os.path.dirname(os.path.realpath(__file__)), "..", "..", "..", "contrib", "valgrind.supp") diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 44047a7fe576..92d787ec444e 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -164,6 +164,8 @@ 'feature_abortnode.py', # vv Tests less than 30s vv 'rpc_quorum.py', + 'wallet_blank.py --legacy-wallet', + 'wallet_blank.py --descriptors', 'wallet_keypool_topup.py --legacy-wallet', 'wallet_keypool_topup.py --descriptors', 'feature_fee_estimation.py', diff --git a/test/functional/tool_wallet.py b/test/functional/tool_wallet.py index c202d44f1b52..6c383b34f81a 100755 --- a/test/functional/tool_wallet.py +++ b/test/functional/tool_wallet.py @@ -18,6 +18,9 @@ ) class ToolWalletTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True diff --git a/test/functional/wallet_abandonconflict.py b/test/functional/wallet_abandonconflict.py index 93450e665011..425fb6b0f890 100755 --- a/test/functional/wallet_abandonconflict.py +++ b/test/functional/wallet_abandonconflict.py @@ -21,6 +21,9 @@ class AbandonConflictTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 self.extra_args = [["-minrelaytxfee=0.00001"], []] diff --git a/test/functional/wallet_avoidreuse.py b/test/functional/wallet_avoidreuse.py index 7b444e05b75c..38864cba1ed3 100755 --- a/test/functional/wallet_avoidreuse.py +++ b/test/functional/wallet_avoidreuse.py @@ -63,6 +63,8 @@ def assert_balances(node, mine, margin=0.001): assert_approx(got[k], v, margin) class AvoidReuseTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) def set_test_params(self): self.num_nodes = 2 diff --git a/test/functional/wallet_backup.py b/test/functional/wallet_backup.py index 281760bd1588..2f113c042140 100755 --- a/test/functional/wallet_backup.py +++ b/test/functional/wallet_backup.py @@ -40,6 +40,9 @@ from test_framework.util import assert_equal, assert_raises_rpc_error class WalletBackupTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True diff --git a/test/functional/wallet_balance.py b/test/functional/wallet_balance.py index 21cb326321d1..b039f147aa3b 100755 --- a/test/functional/wallet_balance.py +++ b/test/functional/wallet_balance.py @@ -47,6 +47,9 @@ def create_transactions(node, address, amt, fees): return txs class WalletTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index e934422c4e1f..5d2f35ea7745 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -24,6 +24,9 @@ class WalletTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 4 if self.options.descriptors: diff --git a/test/functional/wallet_blank.py b/test/functional/wallet_blank.py new file mode 100755 index 000000000000..eda3fda35bbf --- /dev/null +++ b/test/functional/wallet_blank.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. + +import os + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.address import ( + ADDRESS_BCRT1_UNSPENDABLE, + ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR, +) +from test_framework.key import ECKey +from test_framework.util import ( + assert_equal, +) +from test_framework.wallet_util import bytes_to_wif + + +class WalletBlankTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def add_options(self, options): + self.add_wallet_options(options) + + def test_importaddress(self): + if self.options.descriptors: + return + self.log.info("Test that importaddress unsets the blank flag") + self.nodes[0].createwallet(wallet_name="iaddr", disable_private_keys=True, blank=True) + wallet = self.nodes[0].get_wallet_rpc("iaddr") + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["blank"], True) + wallet.importaddress(ADDRESS_BCRT1_UNSPENDABLE) + assert_equal(wallet.getwalletinfo()["blank"], False) + + def test_importpubkey(self): + if self.options.descriptors: + return + self.log.info("Test that importpubkey unsets the blank flag") + for i, comp in enumerate([True, False]): + self.nodes[0].createwallet(wallet_name=f"ipub{i}", disable_private_keys=True, blank=True) + wallet = self.nodes[0].get_wallet_rpc(f"ipub{i}") + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["blank"], True) + + eckey = ECKey() + eckey.generate(compressed=comp) + + wallet.importpubkey(eckey.get_pubkey().get_bytes().hex()) + assert_equal(wallet.getwalletinfo()["blank"], False) + + def test_importprivkey(self): + if self.options.descriptors: + return + self.log.info("Test that importprivkey unsets the blank flag") + for i, comp in enumerate([True, False]): + self.nodes[0].createwallet(wallet_name=f"ipriv{i}", blank=True) + wallet = self.nodes[0].get_wallet_rpc(f"ipriv{i}") + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["blank"], True) + + eckey = ECKey() + eckey.generate(compressed=comp) + wif = bytes_to_wif(eckey.get_bytes(), eckey.is_compressed) + + wallet.importprivkey(wif) + assert_equal(wallet.getwalletinfo()["blank"], False) + + def test_importmulti(self): + if self.options.descriptors: + return + self.log.info("Test that importmulti unsets the blank flag") + self.nodes[0].createwallet(wallet_name="imulti", disable_private_keys=True, blank=True) + wallet = self.nodes[0].get_wallet_rpc("imulti") + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["blank"], True) + wallet.importmulti([{ + "desc": ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR, + "timestamp": "now", + }]) + assert_equal(wallet.getwalletinfo()["blank"], False) + + def test_importdescriptors(self): + if not self.options.descriptors: + return + self.log.info("Test that importdescriptors preserves the blank flag") + self.nodes[0].createwallet(wallet_name="idesc", disable_private_keys=True, blank=True) + wallet = self.nodes[0].get_wallet_rpc("idesc") + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["blank"], True) + wallet.importdescriptors([{ + "desc": ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR, + "timestamp": "now", + }]) + assert_equal(wallet.getwalletinfo()["blank"], True) + + def test_importwallet(self): + if self.options.descriptors: + return + self.log.info("Test that importwallet unsets the blank flag") + def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + + self.nodes[0].createwallet(wallet_name="iwallet", blank=True) + wallet = self.nodes[0].get_wallet_rpc("iwallet") + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["blank"], True) + + wallet_dump_path = os.path.join(self.nodes[0].datadir, "wallet.dump") + def_wallet.dumpwallet(wallet_dump_path) + + wallet.importwallet(wallet_dump_path) + assert_equal(wallet.getwalletinfo()["blank"], False) + + def test_encrypt_legacy(self): + if self.options.descriptors: + return + self.log.info("Test that encrypting a blank legacy wallet preserves the blank flag and does not generate a seed") + self.nodes[0].createwallet(wallet_name="encblanklegacy", blank=True) + wallet = self.nodes[0].get_wallet_rpc("encblanklegacy") + + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], False) + assert_equal(info["blank"], True) + assert "hdseedid" not in info + + wallet.encryptwallet("pass") + info = wallet.getwalletinfo() + assert_equal(info["blank"], True) + assert "hdseedid" not in info + + def test_encrypt_descriptors(self): + if not self.options.descriptors: + return + self.log.info("Test that encrypting a blank descriptor wallet preserves the blank flag and descriptors remain the same") + self.nodes[0].createwallet(wallet_name="encblankdesc", blank=True) + wallet = self.nodes[0].get_wallet_rpc("encblankdesc") + + info = wallet.getwalletinfo() + assert_equal(info["descriptors"], True) + assert_equal(info["blank"], True) + descs = wallet.listdescriptors() + + wallet.encryptwallet("pass") + assert_equal(wallet.getwalletinfo()["blank"], True) + assert_equal(descs, wallet.listdescriptors()) + + def run_test(self): + self.test_importaddress() + self.test_importpubkey() + self.test_importprivkey() + self.test_importmulti() + self.test_importdescriptors() + self.test_importwallet() + self.test_encrypt_legacy() + self.test_encrypt_descriptors() + + +if __name__ == '__main__': + WalletBlankTest().main() diff --git a/test/functional/wallet_coinbase_category.py b/test/functional/wallet_coinbase_category.py index ad66ee8d17b2..b1037ea1342d 100755 --- a/test/functional/wallet_coinbase_category.py +++ b/test/functional/wallet_coinbase_category.py @@ -13,6 +13,9 @@ ) class CoinbaseCategoryTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True diff --git a/test/functional/wallet_create_tx.py b/test/functional/wallet_create_tx.py index 65c43cf7a74e..5e8bf17d16fe 100755 --- a/test/functional/wallet_create_tx.py +++ b/test/functional/wallet_create_tx.py @@ -11,6 +11,9 @@ class CreateTxWalletTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 diff --git a/test/functional/wallet_createwallet.py b/test/functional/wallet_createwallet.py index f8ba2cab7318..f8964ecf428d 100755 --- a/test/functional/wallet_createwallet.py +++ b/test/functional/wallet_createwallet.py @@ -16,6 +16,9 @@ from test_framework.wallet_util import bytes_to_wif, generate_wif_key class CreateWalletTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 diff --git a/test/functional/wallet_crosschain.py b/test/functional/wallet_crosschain.py index a9cf425fcc54..e7945300b0d6 100755 --- a/test/functional/wallet_crosschain.py +++ b/test/functional/wallet_crosschain.py @@ -10,6 +10,9 @@ from test_framework.util import assert_raises_rpc_error class WalletCrossChain(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index eba75ff94108..74d096430ce9 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -12,6 +12,9 @@ class WalletDescriptorTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/wallet_dump.py b/test/functional/wallet_dump.py index 816a812df820..e224ed64d51b 100755 --- a/test/functional/wallet_dump.py +++ b/test/functional/wallet_dump.py @@ -84,6 +84,9 @@ def read_dump(file_name, addrs, script_addrs, hd_master_addr_old): class WalletDumpTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, descriptors=False) + def set_test_params(self): self.num_nodes = 1 self.disable_mocktime = True diff --git a/test/functional/wallet_encryption.py b/test/functional/wallet_encryption.py index fb1459b6808f..090970834a0e 100755 --- a/test/functional/wallet_encryption.py +++ b/test/functional/wallet_encryption.py @@ -14,6 +14,9 @@ class WalletEncryptionTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/wallet_fallbackfee.py b/test/functional/wallet_fallbackfee.py index 674c37dc73c5..04b5f5593732 100755 --- a/test/functional/wallet_fallbackfee.py +++ b/test/functional/wallet_fallbackfee.py @@ -9,6 +9,9 @@ from test_framework.util import assert_raises_rpc_error class WalletRBFTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True diff --git a/test/functional/wallet_groups.py b/test/functional/wallet_groups.py index 4912e97cd1c3..4ee63d803a0d 100755 --- a/test/functional/wallet_groups.py +++ b/test/functional/wallet_groups.py @@ -15,6 +15,9 @@ ) class WalletGroupTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 5 diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index ef981056a5bc..211b5106d17e 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -15,6 +15,9 @@ ) class WalletHDTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 diff --git a/test/functional/wallet_import_rescan.py b/test/functional/wallet_import_rescan.py index 1a42a8611918..ba5d7ab4b116 100755 --- a/test/functional/wallet_import_rescan.py +++ b/test/functional/wallet_import_rescan.py @@ -136,6 +136,9 @@ def get_rand_amount(): class ImportRescanTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, descriptors=False) + def set_test_params(self): self.num_nodes = 2 + len(IMPORT_NODES) self.supports_cli = False diff --git a/test/functional/wallet_import_with_label.py b/test/functional/wallet_import_with_label.py index f00d5c7c6aa7..25a61ad037f3 100755 --- a/test/functional/wallet_import_with_label.py +++ b/test/functional/wallet_import_with_label.py @@ -15,6 +15,9 @@ class ImportWithLabel(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, descriptors=False) + def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index 9d961c69885b..0d24c2565e8a 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -28,6 +28,9 @@ ) class ImportDescriptorsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + def set_test_params(self): self.num_nodes = 2 self.extra_args = [[], diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py index d924a94ed342..d16503671760 100755 --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -34,6 +34,9 @@ ) class ImportMultiTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, descriptors=False) + def set_test_params(self): self.num_nodes = 2 self.setup_clean_chain = True diff --git a/test/functional/wallet_importprunedfunds.py b/test/functional/wallet_importprunedfunds.py index 2dee8c438d66..d701c7d95a11 100755 --- a/test/functional/wallet_importprunedfunds.py +++ b/test/functional/wallet_importprunedfunds.py @@ -21,6 +21,9 @@ class ImportPrunedFundsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 diff --git a/test/functional/wallet_keypool.py b/test/functional/wallet_keypool.py index 71ab9ff73cfa..2b6e68941c3f 100755 --- a/test/functional/wallet_keypool.py +++ b/test/functional/wallet_keypool.py @@ -10,6 +10,9 @@ from test_framework.util import assert_equal, assert_raises_rpc_error class KeyPoolTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 self.extra_args = [['-usehd=0']] diff --git a/test/functional/wallet_keypool_topup.py b/test/functional/wallet_keypool_topup.py index 9ae09cef7801..f699615d95d1 100755 --- a/test/functional/wallet_keypool_topup.py +++ b/test/functional/wallet_keypool_topup.py @@ -21,6 +21,9 @@ class KeypoolRestoreTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 4 diff --git a/test/functional/wallet_labels.py b/test/functional/wallet_labels.py index 0b8511af038d..3b70e4132ad7 100755 --- a/test/functional/wallet_labels.py +++ b/test/functional/wallet_labels.py @@ -18,6 +18,9 @@ class WalletLabelsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index e6031f9c0b75..bd54e5e1b09f 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -18,6 +18,9 @@ class ListDescriptorsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + def set_test_params(self): self.num_nodes = 1 diff --git a/test/functional/wallet_listreceivedby.py b/test/functional/wallet_listreceivedby.py index 6da3f236c89b..76f03d515a78 100755 --- a/test/functional/wallet_listreceivedby.py +++ b/test/functional/wallet_listreceivedby.py @@ -16,6 +16,9 @@ class ReceivedByTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 # whitelist peers to speed up tx relay / mempool sync diff --git a/test/functional/wallet_listsinceblock.py b/test/functional/wallet_listsinceblock.py index 4a4167f11155..c408741635c1 100755 --- a/test/functional/wallet_listsinceblock.py +++ b/test/functional/wallet_listsinceblock.py @@ -19,6 +19,9 @@ from decimal import Decimal class ListSinceBlockTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 4 self.setup_clean_chain = True diff --git a/test/functional/wallet_listtransactions.py b/test/functional/wallet_listtransactions.py index c6d3c2812352..7d59b81ec44c 100755 --- a/test/functional/wallet_listtransactions.py +++ b/test/functional/wallet_listtransactions.py @@ -17,6 +17,9 @@ class ListTransactionsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 3 # This test isn't testing txn relay/timing, so set whitelist on the @@ -103,6 +106,7 @@ def run_test(self): {"txid": txid, "label": "watchonly"}) self.run_externally_generated_address_test() + self.run_coinjoin_test() self.run_invalid_parameters_test() def run_externally_generated_address_test(self): @@ -159,6 +163,34 @@ def normalize_list(txs): assert_equal(['pizza2'], self.nodes[0].getaddressinfo(addr2)['labels']) assert_equal(['pizza3'], self.nodes[0].getaddressinfo(addr3)['labels']) + def run_coinjoin_test(self): + self.log.info('Check "coin-join" transaction') + input_0 = next(i for i in self.nodes[0].listunspent(query_options={"minimumAmount": 0.2}, include_unsafe=False)) + input_1 = next(i for i in self.nodes[1].listunspent(query_options={"minimumAmount": 0.2}, include_unsafe=False)) + raw_hex = self.nodes[0].createrawtransaction( + inputs=[ + { + "txid": input_0["txid"], + "vout": input_0["vout"], + }, + { + "txid": input_1["txid"], + "vout": input_1["vout"], + }, + ], + outputs={ + self.nodes[0].getnewaddress(): 0.123, + self.nodes[1].getnewaddress(): 0.123, + }, + ) + raw_hex = self.nodes[0].signrawtransactionwithwallet(raw_hex)["hex"] + raw_hex = self.nodes[1].signrawtransactionwithwallet(raw_hex)["hex"] + txid_join = self.nodes[0].sendrawtransaction(hexstring=raw_hex, maxfeerate=0) + fee_join = self.nodes[0].getmempoolentry(txid_join)["fees"]["base"] + # Fee should be correct: assert_equal(fee_join, self.nodes[0].gettransaction(txid_join)['fee']) + # But it is not, see for example https://github.com/bitcoin/bitcoin/issues/14136: + assert fee_join != self.nodes[0].gettransaction(txid_join)["fee"] + def run_invalid_parameters_test(self): self.log.info("Test listtransactions RPC parameter validity") assert_raises_rpc_error(-8, 'Label argument must be a valid label name or "*".', self.nodes[0].listtransactions, label="") diff --git a/test/functional/wallet_multisig_descriptor_psbt.py b/test/functional/wallet_multisig_descriptor_psbt.py index d2fb5482822a..ccd89379a6de 100755 --- a/test/functional/wallet_multisig_descriptor_psbt.py +++ b/test/functional/wallet_multisig_descriptor_psbt.py @@ -16,6 +16,9 @@ class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + def set_test_params(self): self.num_nodes = 3 self.setup_clean_chain = True diff --git a/test/functional/wallet_multiwallet.py b/test/functional/wallet_multiwallet.py index 541751768d63..d4a86a8bec60 100755 --- a/test/functional/wallet_multiwallet.py +++ b/test/functional/wallet_multiwallet.py @@ -51,6 +51,7 @@ def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): + self.add_wallet_options(parser) parser.add_argument( '--data_wallets_dir', default=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/wallets/'), diff --git a/test/functional/wallet_orphanedreward.py b/test/functional/wallet_orphanedreward.py index 871ca02a7505..67e39fc3b4ca 100755 --- a/test/functional/wallet_orphanedreward.py +++ b/test/functional/wallet_orphanedreward.py @@ -9,6 +9,9 @@ from decimal import Decimal class OrphanedBlockRewardTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 diff --git a/test/functional/wallet_reorgsrestore.py b/test/functional/wallet_reorgsrestore.py index fbd870338cef..07a4f6b03d0f 100755 --- a/test/functional/wallet_reorgsrestore.py +++ b/test/functional/wallet_reorgsrestore.py @@ -23,6 +23,9 @@ ) class ReorgsRestoreTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 3 diff --git a/test/functional/wallet_resendwallettransactions.py b/test/functional/wallet_resendwallettransactions.py index 50a513ea83bd..8356b89d401b 100755 --- a/test/functional/wallet_resendwallettransactions.py +++ b/test/functional/wallet_resendwallettransactions.py @@ -14,6 +14,9 @@ class ResendWalletTransactionsTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 diff --git a/test/functional/wallet_send.py b/test/functional/wallet_send.py index 8ff83175abc3..cdc3ef3ee082 100755 --- a/test/functional/wallet_send.py +++ b/test/functional/wallet_send.py @@ -20,6 +20,9 @@ from test_framework.wallet_util import bytes_to_wif class WalletSendTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 2 # whitelist all peers to speed up tx relay / mempool sync diff --git a/test/functional/wallet_signer.py b/test/functional/wallet_signer.py index cbb59bede125..c3e09b06d290 100755 --- a/test/functional/wallet_signer.py +++ b/test/functional/wallet_signer.py @@ -18,6 +18,9 @@ class WalletSignerTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, legacy=False) + def mock_signer_path(self): path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'mocks', 'signer.py') if platform.system() == "Windows": diff --git a/test/functional/wallet_signmessagewithaddress.py b/test/functional/wallet_signmessagewithaddress.py index df5e36bf3dde..0ec1ced1ce87 100755 --- a/test/functional/wallet_signmessagewithaddress.py +++ b/test/functional/wallet_signmessagewithaddress.py @@ -10,6 +10,9 @@ ) class SignMessagesWithAddressTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/wallet_signrawtransactionwithwallet.py b/test/functional/wallet_signrawtransactionwithwallet.py index aaf02e7b5e7e..880ed8a594e1 100755 --- a/test/functional/wallet_signrawtransactionwithwallet.py +++ b/test/functional/wallet_signrawtransactionwithwallet.py @@ -12,6 +12,9 @@ from decimal import Decimal, getcontext class SignRawTransactionWithWalletTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/wallet_simulaterawtx.py b/test/functional/wallet_simulaterawtx.py index b2da65bdcfff..fbaa26453404 100755 --- a/test/functional/wallet_simulaterawtx.py +++ b/test/functional/wallet_simulaterawtx.py @@ -15,6 +15,9 @@ ) class SimulateTxTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/wallet_startup.py b/test/functional/wallet_startup.py index 742a91effe54..1e0c753e4d3a 100755 --- a/test/functional/wallet_startup.py +++ b/test/functional/wallet_startup.py @@ -13,6 +13,9 @@ class WalletStartupTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 diff --git a/test/functional/wallet_timelock.py b/test/functional/wallet_timelock.py index a71cec6607de..57a7c3907ade 100755 --- a/test/functional/wallet_timelock.py +++ b/test/functional/wallet_timelock.py @@ -8,6 +8,9 @@ class WalletLocktimeTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 diff --git a/test/functional/wallet_transactiontime_rescan.py b/test/functional/wallet_transactiontime_rescan.py index 07602337bd3d..268e623cd90d 100755 --- a/test/functional/wallet_transactiontime_rescan.py +++ b/test/functional/wallet_transactiontime_rescan.py @@ -17,6 +17,9 @@ class TransactionTimeRescanTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.disable_mocktime = True self.setup_clean_chain = False diff --git a/test/functional/wallet_txn_clone.py b/test/functional/wallet_txn_clone.py index 130e5d3b74f7..cc2a2488dfe4 100755 --- a/test/functional/wallet_txn_clone.py +++ b/test/functional/wallet_txn_clone.py @@ -24,6 +24,7 @@ def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): + self.add_wallet_options(parser) parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true", help="Test double-spend of 1-confirmed transaction") diff --git a/test/functional/wallet_txn_doublespend.py b/test/functional/wallet_txn_doublespend.py index a33720a4af3a..c1fce47b728b 100755 --- a/test/functional/wallet_txn_doublespend.py +++ b/test/functional/wallet_txn_doublespend.py @@ -22,6 +22,7 @@ def skip_test_if_missing_module(self): self.skip_if_no_wallet() def add_options(self, parser): + self.add_wallet_options(parser) parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true", help="Test double-spend of 1-confirmed transaction") diff --git a/test/functional/wallet_upgradewallet.py b/test/functional/wallet_upgradewallet.py index 498834304f70..49c8469a599c 100755 --- a/test/functional/wallet_upgradewallet.py +++ b/test/functional/wallet_upgradewallet.py @@ -26,6 +26,9 @@ UPGRADED_KEYMETA_VERSION = 12 class UpgradeWalletTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, descriptors=False) + def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 diff --git a/test/functional/wallet_watchonly.py b/test/functional/wallet_watchonly.py index 7532cf9a5b2b..89eaa050baca 100755 --- a/test/functional/wallet_watchonly.py +++ b/test/functional/wallet_watchonly.py @@ -14,6 +14,9 @@ class CreateWalletWatchonlyTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser) + def set_test_params(self): self.num_nodes = 1 self.supports_cli = True