From 342abfb3f4368fcdb67f3002c5558d4106d9bf83 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Wed, 28 Dec 2022 13:38:37 +0100 Subject: [PATCH 01/19] wallet: fully migrate address book entries for watchonly/solvable wallets Currently `migratewallet` migrates the address book (i.e. labels and purposes) for watchonly and solvable wallets only in RAM, but doesn't persist them on disk. Fix this by adding another loop for both of the special wallet types after which writes the corresponding NAME and PURPOSE entries to the database in a single batch. Github-Pull: #26761 Rebased-From: d5f4ae7fac0bceb0c9ad939b9a4fbdb85da0bf95 --- src/wallet/wallet.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 36fe32e54dd..0f95739ec18 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3918,6 +3918,23 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error) } } } + + // Persist added address book entries (labels, purpose) for watchonly and solvable wallets + auto persist_address_book = [](const CWallet& wallet) { + LOCK(wallet.cs_wallet); + WalletBatch batch{wallet.GetDatabase()}; + for (const auto& [destination, addr_book_data] : wallet.m_address_book) { + auto address{EncodeDestination(destination)}; + auto purpose{addr_book_data.purpose}; + auto label{addr_book_data.GetLabel()}; + // don't bother writing default values (unknown purpose, empty label) + if (purpose != "unknown") batch.WritePurpose(address, purpose); + if (!label.empty()) batch.WriteName(address, label); + } + }; + if (data.watchonly_wallet) persist_address_book(*data.watchonly_wallet); + if (data.solvable_wallet) persist_address_book(*data.solvable_wallet); + // Remove the things to delete if (dests_to_delete.size() > 0) { for (const auto& dest : dests_to_delete) { From cbcdafa471da3d1edd183143ae9d433627ef16dd Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Wed, 28 Dec 2022 13:41:49 +0100 Subject: [PATCH 02/19] test: wallet: check that labels are migrated to watchonly wallet Github-Pull: #26761 Rebased-From: 730e14a317ae45fe871c8d6f44a51936756bbbea --- test/functional/wallet_migration.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py index 4f060f99606..2af8b664b3c 100755 --- a/test/functional/wallet_migration.py +++ b/test/functional/wallet_migration.py @@ -257,7 +257,7 @@ class WalletMigrationTest(BitcoinTestFramework): imports0 = self.nodes[0].get_wallet_rpc("imports0") assert_equal(imports0.getwalletinfo()["descriptors"], False) - # Exteranl address label + # External address label imports0.setlabel(default.getnewaddress(), "external") # Normal non-watchonly tx @@ -310,6 +310,13 @@ class WalletMigrationTest(BitcoinTestFramework): assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", watchonly.gettransaction, received_txid) assert_equal(len(watchonly.listtransactions(include_watchonly=True)), 3) + # Check that labels were migrated and persisted to watchonly wallet + self.nodes[0].unloadwallet("imports0_watchonly") + self.nodes[0].loadwallet("imports0_watchonly") + labels = watchonly.listlabels() + assert "external" in labels + assert "imported" in labels + def test_no_privkeys(self): default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) From 428dcd51e6adab564ffb87ed678317924868572f Mon Sep 17 00:00:00 2001 From: Andrew Chow Date: Fri, 9 Dec 2022 17:13:54 -0500 Subject: [PATCH 03/19] wallet: Skip rescanning if wallet is more recent than tip If a wallet has key birthdates that are more recent than the currrent chain tip, or a bestblock height higher than the current tip, we should not attempt to rescan as there is nothing to scan for. Github-Pull: #26679 Rebased-From: 378400953424598fd78ccec5ba8cc38bc253c550 --- src/wallet/wallet.cpp | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 0f95739ec18..2cab85ceea5 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3102,6 +3102,24 @@ bool CWallet::AttachChain(const std::shared_ptr& walletInstance, interf if (tip_height && *tip_height != rescan_height) { + // No need to read and scan block if block was created before + // our wallet birthday (as adjusted for block time variability) + std::optional time_first_key; + for (auto spk_man : walletInstance->GetAllScriptPubKeyMans()) { + int64_t time = spk_man->GetTimeFirstKey(); + if (!time_first_key || time < *time_first_key) time_first_key = time; + } + if (time_first_key) { + FoundBlock found = FoundBlock().height(rescan_height); + chain.findFirstBlockWithTimeAndHeight(*time_first_key - TIMESTAMP_WINDOW, rescan_height, found); + if (!found.found) { + // We were unable to find a block that had a time more recent than our earliest timestamp + // or a height higher than the wallet was synced to, indicating that the wallet is newer than the + // current chain tip. Skip rescanning in this case. + rescan_height = *tip_height; + } + } + // Technically we could execute the code below in any case, but performing the // `while` loop below can make startup very slow, so only check blocks on disk // if necessary. @@ -3134,17 +3152,6 @@ bool CWallet::AttachChain(const std::shared_ptr& walletInstance, interf chain.initMessage(_("Rescanning…").translated); walletInstance->WalletLogPrintf("Rescanning last %i blocks (from block %i)...\n", *tip_height - rescan_height, rescan_height); - // No need to read and scan block if block was created before - // our wallet birthday (as adjusted for block time variability) - std::optional time_first_key; - for (auto spk_man : walletInstance->GetAllScriptPubKeyMans()) { - int64_t time = spk_man->GetTimeFirstKey(); - if (!time_first_key || time < *time_first_key) time_first_key = time; - } - if (time_first_key) { - chain.findFirstBlockWithTimeAndHeight(*time_first_key - TIMESTAMP_WINDOW, rescan_height, FoundBlock().height(rescan_height)); - } - { WalletRescanReserver reserver(*walletInstance); if (!reserver.reserve() || (ScanResult::SUCCESS != walletInstance->ScanForWalletTransactions(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/true).status)) { From 5c824ac5e1e35f77e323319849b03ac9d8cf45d3 Mon Sep 17 00:00:00 2001 From: John Moffett Date: Fri, 9 Dec 2022 13:31:27 -0500 Subject: [PATCH 04/19] For feebump, ignore abandoned descendant spends To be eligible for fee-bumping, a transaction must not have any of its outputs (eg - change) spent in other unconfirmed transactions in the wallet. However, this check should not apply to abandoned transactions. A new test case is added to cover this case. Github-Pull: #26675 Rebased-From: f9ce0eadf4eb58d1e2207c27fabe69a5642482e7 --- src/wallet/wallet.cpp | 3 +-- test/functional/wallet_bumpfee.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 2cab85ceea5..9149152bb0f 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -589,8 +589,7 @@ bool CWallet::HasWalletSpend(const CTransactionRef& tx) const AssertLockHeld(cs_wallet); const uint256& txid = tx->GetHash(); for (unsigned int i = 0; i < tx->vout.size(); ++i) { - auto iter = mapTxSpends.find(COutPoint(txid, i)); - if (iter != mapTxSpends.end()) { + if (IsSpent(COutPoint(txid, i))) { return true; } } diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py index f4ae6972927..016992dbea8 100755 --- a/test/functional/wallet_bumpfee.py +++ b/test/functional/wallet_bumpfee.py @@ -89,6 +89,7 @@ class BumpFeeTest(BitcoinTestFramework): test_nonrbf_bumpfee_fails(self, peer_node, dest_address) test_notmine_bumpfee(self, rbf_node, peer_node, dest_address) test_bumpfee_with_descendant_fails(self, rbf_node, rbf_node_address, dest_address) + test_bumpfee_with_abandoned_descendant_succeeds(self, rbf_node, rbf_node_address, dest_address) test_dust_to_fee(self, rbf_node, dest_address) test_watchonly_psbt(self, peer_node, rbf_node, dest_address) test_rebumping(self, rbf_node, dest_address) @@ -294,6 +295,35 @@ def test_bumpfee_with_descendant_fails(self, rbf_node, rbf_node_address, dest_ad self.clear_mempool() +def test_bumpfee_with_abandoned_descendant_succeeds(self, rbf_node, rbf_node_address, dest_address): + self.log.info('Test that fee can be bumped when it has abandoned descendant') + # parent is send-to-self, so we don't have to check which output is change when creating the child tx + parent_id = spend_one_input(rbf_node, rbf_node_address) + # Submit child transaction with low fee + child_id = rbf_node.send(outputs={dest_address: 0.00020000}, + options={"inputs": [{"txid": parent_id, "vout": 0}], "fee_rate": 2})["txid"] + assert child_id in rbf_node.getrawmempool() + + # Restart the node with higher min relay fee so the descendant tx is no longer in mempool so that we can abandon it + self.restart_node(1, ['-minrelaytxfee=0.00005'] + self.extra_args[1]) + rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT) + self.connect_nodes(1, 0) + assert parent_id in rbf_node.getrawmempool() + assert child_id not in rbf_node.getrawmempool() + # Should still raise an error even if not in mempool + assert_raises_rpc_error(-8, "Transaction has descendants in the wallet", rbf_node.bumpfee, parent_id) + # Now abandon the child transaction and bump the original + rbf_node.abandontransaction(child_id) + bumped_result = rbf_node.bumpfee(parent_id, {"fee_rate": HIGH}) + assert bumped_result['txid'] in rbf_node.getrawmempool() + assert parent_id not in rbf_node.getrawmempool() + # Cleanup + self.restart_node(1, self.extra_args[1]) + rbf_node.walletpassphrase(WALLET_PASSPHRASE, WALLET_PASSPHRASE_TIMEOUT) + self.connect_nodes(1, 0) + self.clear_mempool() + + def test_small_output_with_feerate_succeeds(self, rbf_node, dest_address): self.log.info('Testing small output with feerate bump succeeds') From 91f83dbeb197fc0fff574d9e29b4560b1d236bec Mon Sep 17 00:00:00 2001 From: Martin Zumsande Date: Sun, 15 Jan 2023 20:18:11 -0500 Subject: [PATCH 05/19] hash: add HashedSourceWriter This class is the counterpart to CHashVerifier, in that it writes data to an underlying source stream, while keeping a hash of the written data. Github-Pull: #26909 Rebased-From: da6c7aeca38e1d0ab5839a374c26af0504d603fc --- src/hash.h | 25 +++++++++++++++++++++++++ src/test/streams_tests.cpp | 14 ++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/hash.h b/src/hash.h index b1ff3acc7df..50255705e48 100644 --- a/src/hash.h +++ b/src/hash.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_HASH_H #define BITCOIN_HASH_H +#include #include #include #include @@ -199,6 +200,30 @@ public: } }; +/** Writes data to an underlying source stream, while hashing the written data. */ +template +class HashedSourceWriter : public CHashWriter +{ +private: + Source& m_source; + +public: + explicit HashedSourceWriter(Source& source LIFETIMEBOUND) : CHashWriter{source.GetType(), source.GetVersion()}, m_source{source} {} + + void write(Span src) + { + m_source.write(src); + CHashWriter::write(src); + } + + template + HashedSourceWriter& operator<<(const T& obj) + { + ::Serialize(*this, obj); + return *this; + } +}; + /** Compute the 256-bit hash of an object's serialization. */ template uint256 SerializeHash(const T& obj, int nType=SER_GETHASH, int nVersion=PROTOCOL_VERSION) diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index 0925e2e9ee4..e30ae845ef0 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -441,4 +441,18 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand) fs::remove(streams_test_filename); } +BOOST_AUTO_TEST_CASE(streams_hashed) +{ + CDataStream stream(SER_NETWORK, INIT_PROTO_VERSION); + HashedSourceWriter hash_writer{stream}; + const std::string data{"bitcoin"}; + hash_writer << data; + + CHashVerifier hash_verifier{&stream}; + std::string result; + hash_verifier >> result; + BOOST_CHECK_EQUAL(data, result); + BOOST_CHECK_EQUAL(hash_writer.GetHash(), hash_verifier.GetHash()); +} + BOOST_AUTO_TEST_SUITE_END() From 07397cdedeffb4da0aedd454d4539d65a0204291 Mon Sep 17 00:00:00 2001 From: Martin Zumsande Date: Fri, 13 Jan 2023 14:12:25 -0500 Subject: [PATCH 06/19] addrdb: Only call Serialize() once The previous logic would call it once for serializing into the filestream, and then again for serializing into the hasher. If AddrMan was changed in between these calls by another thread, the resulting peers.dat would be corrupt with non-matching checksum and data. Fix this by using HashedSourceWriter, which writes the data to the underlying stream and keeps track of the hash in one go. Github-Pull: #26909 Rebased-From: 5eabb61b2386d00e93e6bbb2f493a56d1b326ad9 --- src/addrdb.cpp | 7 +++---- src/addrman.cpp | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/addrdb.cpp b/src/addrdb.cpp index 7106d819b0f..554f20adc55 100644 --- a/src/addrdb.cpp +++ b/src/addrdb.cpp @@ -34,10 +34,9 @@ bool SerializeDB(Stream& stream, const Data& data) { // Write and commit header, data try { - CHashWriter hasher(stream.GetType(), stream.GetVersion()); - stream << Params().MessageStart() << data; - hasher << Params().MessageStart() << data; - stream << hasher.GetHash(); + HashedSourceWriter hashwriter{stream}; + hashwriter << Params().MessageStart() << data; + stream << hashwriter.GetHash(); } catch (const std::exception& e) { return error("%s: Serialize or I/O error - %s", __func__, e.what()); } diff --git a/src/addrman.cpp b/src/addrman.cpp index f16ff2230b9..b281344b045 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -1178,8 +1178,7 @@ void AddrMan::Unserialize(Stream& s_) } // explicit instantiation -template void AddrMan::Serialize(CHashWriter& s) const; -template void AddrMan::Serialize(CAutoFile& s) const; +template void AddrMan::Serialize(HashedSourceWriter& s) const; template void AddrMan::Serialize(CDataStream& s) const; template void AddrMan::Unserialize(CAutoFile& s); template void AddrMan::Unserialize(CHashVerifier& s); From 7cf73dfed5757819c0a5485ae05e8e1a57528a0e Mon Sep 17 00:00:00 2001 From: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz> Date: Thu, 19 Jan 2023 19:35:43 +0100 Subject: [PATCH 07/19] Add missing includes to fix gcc-13 compile error Github-Pull: #26924 Rebased-From: fadeb6b103cb441e0e91ef506ef29febabb10715 --- src/support/lockedpool.cpp | 3 +++ src/support/lockedpool.h | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/support/lockedpool.cpp b/src/support/lockedpool.cpp index e48accf0a47..ea2fbd706a1 100644 --- a/src/support/lockedpool.cpp +++ b/src/support/lockedpool.cpp @@ -19,6 +19,9 @@ #endif #include +#include +#include +#include #ifdef ARENA_DEBUG #include #include diff --git a/src/support/lockedpool.h b/src/support/lockedpool.h index 03e4e371a3a..66fbc218abf 100644 --- a/src/support/lockedpool.h +++ b/src/support/lockedpool.h @@ -5,11 +5,11 @@ #ifndef BITCOIN_SUPPORT_LOCKEDPOOL_H #define BITCOIN_SUPPORT_LOCKEDPOOL_H -#include +#include #include #include -#include #include +#include #include /** From cff67180b3ba9ab53e01d44769059aa5559c01f7 Mon Sep 17 00:00:00 2001 From: fanquake Date: Sun, 22 Jan 2023 15:58:04 +0000 Subject: [PATCH 08/19] depends: fix systemtap download URL Github-Pull: #26944 Rebased-From: d81ca6619a5d05472af7f59e36cd100dd04a3a01 --- depends/packages/systemtap.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/packages/systemtap.mk b/depends/packages/systemtap.mk index a57f1b6d36a..ad74323d983 100644 --- a/depends/packages/systemtap.mk +++ b/depends/packages/systemtap.mk @@ -1,6 +1,6 @@ package=systemtap $(package)_version=4.7 -$(package)_download_path=https://sourceware.org/systemtap/ftp/releases/ +$(package)_download_path=https://sourceware.org/ftp/systemtap/releases/ $(package)_file_name=$(package)-$($(package)_version).tar.gz $(package)_sha256_hash=43a0a3db91aa4d41e28015b39a65e62059551f3cc7377ebf3a3a5ca7339e7b1f $(package)_patches=remove_SDT_ASM_SECTION_AUTOGROUP_SUPPORT_check.patch From b7e242ecb3aa0074aea753e5bc9f8d22674e8294 Mon Sep 17 00:00:00 2001 From: John Moffett Date: Thu, 26 Jan 2023 10:19:11 -0500 Subject: [PATCH 09/19] Correctly limit overview transaction list The way that the main overview page limits the number of transactions displayed (currently 5) is not an appropriate use of Qt. If it's run with a DEBUG build of Qt, it'll result in a segfault in certain relatively common situations. Instead of artificially limiting the rowCount() in the subclassed proxy filter, we hide/unhide the rows in the displaying QListView upon any changes in the sorted proxy filter. Github-Pull: bitcoin-core/gui/pull/704 Rebased-From: 08209c039ff4ca5be4982da7a2ab7a624117ce1a --- src/qt/overviewpage.cpp | 15 ++++++++++++++- src/qt/overviewpage.h | 1 + src/qt/transactionfilterproxy.cpp | 18 ------------------ src/qt/transactionfilterproxy.h | 6 ------ 4 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 85a3c36f399..d47afa98744 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -264,7 +264,6 @@ void OverviewPage::setWalletModel(WalletModel *model) // Set up transaction list filter.reset(new TransactionFilterProxy()); filter->setSourceModel(model->getTransactionTableModel()); - filter->setLimit(NUM_ITEMS); filter->setDynamicSortFilter(true); filter->setSortRole(Qt::EditRole); filter->setShowInactive(false); @@ -273,6 +272,10 @@ void OverviewPage::setWalletModel(WalletModel *model) ui->listTransactions->setModel(filter.get()); ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress); + connect(filter.get(), &TransactionFilterProxy::rowsInserted, this, &OverviewPage::LimitTransactionRows); + connect(filter.get(), &TransactionFilterProxy::rowsRemoved, this, &OverviewPage::LimitTransactionRows); + connect(filter.get(), &TransactionFilterProxy::rowsMoved, this, &OverviewPage::LimitTransactionRows); + LimitTransactionRows(); // Keep up to date with wallet setBalance(model->getCachedBalance()); connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance); @@ -301,6 +304,16 @@ void OverviewPage::changeEvent(QEvent* e) QWidget::changeEvent(e); } +// Only show most recent NUM_ITEMS rows +void OverviewPage::LimitTransactionRows() +{ + if (filter && ui->listTransactions && ui->listTransactions->model() && filter.get() == ui->listTransactions->model()) { + for (int i = 0; i < filter->rowCount(); ++i) { + ui->listTransactions->setRowHidden(i, i >= NUM_ITEMS); + } + } +} + void OverviewPage::updateDisplayUnit() { if (walletModel && walletModel->getOptionsModel()) { diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index 56f45907dba..e171ddf06ab 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -60,6 +60,7 @@ private: std::unique_ptr filter; private Q_SLOTS: + void LimitTransactionRows(); void updateDisplayUnit(); void handleTransactionClicked(const QModelIndex &index); void updateAlerts(const QString &warnings); diff --git a/src/qt/transactionfilterproxy.cpp b/src/qt/transactionfilterproxy.cpp index 3be7e1a969b..33ca74231fb 100644 --- a/src/qt/transactionfilterproxy.cpp +++ b/src/qt/transactionfilterproxy.cpp @@ -17,7 +17,6 @@ TransactionFilterProxy::TransactionFilterProxy(QObject *parent) : typeFilter(ALL_TYPES), watchOnlyFilter(WatchOnlyFilter_All), minAmount(0), - limitRows(-1), showInactive(true) { } @@ -92,25 +91,8 @@ void TransactionFilterProxy::setWatchOnlyFilter(WatchOnlyFilter filter) invalidateFilter(); } -void TransactionFilterProxy::setLimit(int limit) -{ - this->limitRows = limit; -} - void TransactionFilterProxy::setShowInactive(bool _showInactive) { this->showInactive = _showInactive; invalidateFilter(); } - -int TransactionFilterProxy::rowCount(const QModelIndex &parent) const -{ - if(limitRows != -1) - { - return std::min(QSortFilterProxyModel::rowCount(parent), limitRows); - } - else - { - return QSortFilterProxyModel::rowCount(parent); - } -} diff --git a/src/qt/transactionfilterproxy.h b/src/qt/transactionfilterproxy.h index fd9be52842e..9b43981bc20 100644 --- a/src/qt/transactionfilterproxy.h +++ b/src/qt/transactionfilterproxy.h @@ -42,14 +42,9 @@ public: void setMinAmount(const CAmount& minimum); void setWatchOnlyFilter(WatchOnlyFilter filter); - /** Set maximum number of rows returned, -1 if unlimited. */ - void setLimit(int limit); - /** Set whether to show conflicted transactions. */ void setShowInactive(bool showInactive); - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - protected: bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; @@ -60,7 +55,6 @@ private: quint32 typeFilter; WatchOnlyFilter watchOnlyFilter; CAmount minAmount; - int limitRows; bool showInactive; }; From 64e7db6f4f256656f4d78a96b07e51f7d5c6d526 Mon Sep 17 00:00:00 2001 From: John Moffett Date: Fri, 10 Feb 2023 16:13:40 -0500 Subject: [PATCH 10/19] Zero out wallet master key upon lock When an encrypted wallet is locked (for instance via the RPC `walletlock`), the docs indicate that the key is removed from memory. However, the vector (with a secure allocator) is merely cleared. This allows the key to persist indefinitely in memory. Instead, manually fill the bytes with zeroes before clearing. Github-Pull: #27080 Rebased-From: 3a11adc7004d21b3dfe028b190d83add31691c55 --- src/wallet/wallet.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 9149152bb0f..5d77b4ed8fe 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -25,6 +25,7 @@ #include