aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorglozow <gloriajzhao@gmail.com>2023-02-27 17:53:20 +0000
committerglozow <gloriajzhao@gmail.com>2023-02-27 18:09:31 +0000
commitc8c85ca16e6abbf9072dee9e526f09b6fcb35037 (patch)
tree2bf39d29063a9f2d22c2573d84390652b2593e59
parent2b87e905857ba456db2cb3d0297a3102f17417be (diff)
parent784a754aa47ce10c6fd99c09cdfc76ee9bc91652 (diff)
downloadbitcoin-c8c85ca16e6abbf9072dee9e526f09b6fcb35037.tar.xz
Merge bitcoin/bitcoin#26878: [24.x] Backports
784a754aa47ce10c6fd99c09cdfc76ee9bc91652 wallet, rpc: Update migratewallet help text for encrypted wallets (Andrew Chow) debcfe313a22fddc2a6247c55f3c7893f6134c05 tests: Tests for migrating wallets by name, and providing passphrase (Andrew Chow) ccc72fecd7287471bf8c3858a4e6c2ddadb64863 wallet: Be able to unlock the wallet for migration (Andrew Chow) 50dd8b13dfc070b550680617e76eff1c09af3a58 rpc: Allow users to specify wallet name for migratewallet (Andrew Chow) 648b06256da65b4513977e351aabf3e5c06ce060 wallet: Allow MigrateLegacyToDescriptor to take a wallet name (Andrew Chow) ab3bd457cdb5c5ee2626081d703d675c2573f28a i2p: use consistent number of tunnels with i2pd and Java I2P (Vasil Dimov) 29cdf42226c0983076701552b351f08351dd54a5 i2p: lower the number of tunnels for transient sessions (Vasil Dimov) 5027e93b6a1e5a5c87b9450c60e2a4572eb32653 i2p: reuse created I2P sessions if not used (Vasil Dimov) a62c541ae8a579d19b8fc55bbb922f0939ab6110 wallet: reuse change dest when recreating TX with avoidpartialspends (Matthew Zipkin) 64e7db6f4f256656f4d78a96b07e51f7d5c6d526 Zero out wallet master key upon lock (John Moffett) b7e242ecb3aa0074aea753e5bc9f8d22674e8294 Correctly limit overview transaction list (John Moffett) cff67180b3ba9ab53e01d44769059aa5559c01f7 depends: fix systemtap download URL (fanquake) 7cf73dfed5757819c0a5485ae05e8e1a57528a0e Add missing includes to fix gcc-13 compile error (MarcoFalke) 07397cdedeffb4da0aedd454d4539d65a0204291 addrdb: Only call Serialize() once (Martin Zumsande) 91f83dbeb197fc0fff574d9e29b4560b1d236bec hash: add HashedSourceWriter (Martin Zumsande) 5c824ac5e1e35f77e323319849b03ac9d8cf45d3 For feebump, ignore abandoned descendant spends (John Moffett) 428dcd51e6adab564ffb87ed678317924868572f wallet: Skip rescanning if wallet is more recent than tip (Andrew Chow) cbcdafa471da3d1edd183143ae9d433627ef16dd test: wallet: check that labels are migrated to watchonly wallet (Sebastian Falbesoner) 342abfb3f4368fcdb67f3002c5558d4106d9bf83 wallet: fully migrate address book entries for watchonly/solvable wallets (Sebastian Falbesoner) Pull request description: Backports: * https://github.com/bitcoin/bitcoin/pull/26595 * https://github.com/bitcoin/bitcoin/pull/26675 * https://github.com/bitcoin/bitcoin/pull/26679 * https://github.com/bitcoin/bitcoin/pull/26761 * https://github.com/bitcoin/bitcoin/pull/26837 * https://github.com/bitcoin/bitcoin/pull/26909 * https://github.com/bitcoin/bitcoin/pull/26924 * https://github.com/bitcoin/bitcoin/pull/26944 * https://github.com/bitcoin-core/gui/pull/704 * https://github.com/bitcoin/bitcoin/pull/27053 * https://github.com/bitcoin/bitcoin/pull/27080 ACKs for top commit: instagibbs: ACK https://github.com/bitcoin/bitcoin/pull/26878/commits/784a754aa47ce10c6fd99c09cdfc76ee9bc91652 achow101: ACK 784a754aa47ce10c6fd99c09cdfc76ee9bc91652 hebasto: ACK 784a754aa47ce10c6fd99c09cdfc76ee9bc91652, I've made backporting locally and got a diff between my branch and this PR as follows: Tree-SHA512: 8ea84aa02d7907ff1e202e1302b441ce9ed2198bf383620ad40056a5d7e8ea88e1047abef0b92d85648016bf9b3195c974be3806ccebd85bef4f85c326869e43
-rw-r--r--depends/packages/systemtap.mk2
-rw-r--r--src/addrdb.cpp7
-rw-r--r--src/addrman.cpp3
-rw-r--r--src/hash.h25
-rw-r--r--src/i2p.cpp7
-rw-r--r--src/net.cpp23
-rw-r--r--src/net.h33
-rw-r--r--src/qt/overviewpage.cpp15
-rw-r--r--src/qt/overviewpage.h1
-rw-r--r--src/qt/transactionfilterproxy.cpp18
-rw-r--r--src/qt/transactionfilterproxy.h6
-rw-r--r--src/support/lockedpool.cpp3
-rw-r--r--src/support/lockedpool.h4
-rw-r--r--src/test/streams_tests.cpp14
-rw-r--r--src/wallet/rpc/wallet.cpp31
-rw-r--r--src/wallet/spend.cpp7
-rw-r--r--src/wallet/wallet.cpp107
-rw-r--r--src/wallet/wallet.h2
-rwxr-xr-xtest/functional/test_runner.py2
-rwxr-xr-xtest/functional/wallet_bumpfee.py30
-rwxr-xr-xtest/functional/wallet_change_address.py105
-rwxr-xr-xtest/functional/wallet_importdescriptors.py18
-rwxr-xr-xtest/functional/wallet_migration.py79
23 files changed, 447 insertions, 95 deletions
diff --git a/depends/packages/systemtap.mk b/depends/packages/systemtap.mk
index a57f1b6d36..ad74323d98 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
diff --git a/src/addrdb.cpp b/src/addrdb.cpp
index 7106d819b0..554f20adc5 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 f16ff2230b..b281344b04 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<CAutoFile>& s) const;
template void AddrMan::Serialize(CDataStream& s) const;
template void AddrMan::Unserialize(CAutoFile& s);
template void AddrMan::Unserialize(CHashVerifier<CAutoFile>& s);
diff --git a/src/hash.h b/src/hash.h
index b1ff3acc7d..50255705e4 100644
--- a/src/hash.h
+++ b/src/hash.h
@@ -6,6 +6,7 @@
#ifndef BITCOIN_HASH_H
#define BITCOIN_HASH_H
+#include <attributes.h>
#include <crypto/common.h>
#include <crypto/ripemd160.h>
#include <crypto/sha256.h>
@@ -199,6 +200,30 @@ public:
}
};
+/** Writes data to an underlying source stream, while hashing the written data. */
+template <typename Source>
+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<const std::byte> src)
+ {
+ m_source.write(src);
+ CHashWriter::write(src);
+ }
+
+ template <typename T>
+ HashedSourceWriter& operator<<(const T& obj)
+ {
+ ::Serialize(*this, obj);
+ return *this;
+ }
+};
+
/** Compute the 256-bit hash of an object's serialization. */
template<typename T>
uint256 SerializeHash(const T& obj, int nType=SER_GETHASH, int nVersion=PROTOCOL_VERSION)
diff --git a/src/i2p.cpp b/src/i2p.cpp
index 28be8009dc..b3a6123167 100644
--- a/src/i2p.cpp
+++ b/src/i2p.cpp
@@ -379,7 +379,9 @@ void Session::CreateIfNotCreatedAlready()
// in the reply in DESTINATION=.
const Reply& reply = SendRequestAndGetReply(
*sock,
- strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7", session_id));
+ strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=TRANSIENT SIGNATURE_TYPE=7 "
+ "inbound.quantity=1 outbound.quantity=1",
+ session_id));
m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
} else {
@@ -395,7 +397,8 @@ void Session::CreateIfNotCreatedAlready()
const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
SendRequestAndGetReply(*sock,
- strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s",
+ strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s "
+ "inbound.quantity=3 outbound.quantity=3",
session_id,
private_key_b64));
}
diff --git a/src/net.cpp b/src/net.cpp
index 0cc14b1d2a..a6bf2a9dcc 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -436,6 +436,7 @@ static CAddress GetBindAddress(const Sock& sock)
CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type)
{
+ AssertLockNotHeld(m_unused_i2p_sessions_mutex);
assert(conn_type != ConnectionType::INBOUND);
if (pszDest == nullptr) {
@@ -496,8 +497,23 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
if (m_i2p_sam_session) {
connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed);
} else {
- i2p_transient_session = std::make_unique<i2p::sam::Session>(proxy.proxy, &interruptNet);
+ {
+ LOCK(m_unused_i2p_sessions_mutex);
+ if (m_unused_i2p_sessions.empty()) {
+ i2p_transient_session =
+ std::make_unique<i2p::sam::Session>(proxy.proxy, &interruptNet);
+ } else {
+ i2p_transient_session.swap(m_unused_i2p_sessions.front());
+ m_unused_i2p_sessions.pop();
+ }
+ }
connected = i2p_transient_session->Connect(addrConnect, conn, proxyConnectionFailed);
+ if (!connected) {
+ LOCK(m_unused_i2p_sessions_mutex);
+ if (m_unused_i2p_sessions.size() < MAX_UNUSED_I2P_SESSIONS_SIZE) {
+ m_unused_i2p_sessions.emplace(i2p_transient_session.release());
+ }
+ }
}
if (connected) {
@@ -1048,6 +1064,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr<Sock>&& sock,
bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type)
{
+ AssertLockNotHeld(m_unused_i2p_sessions_mutex);
std::optional<int> max_connections;
switch (conn_type) {
case ConnectionType::INBOUND:
@@ -1510,6 +1527,7 @@ void CConnman::DumpAddresses()
void CConnman::ProcessAddrFetch()
{
+ AssertLockNotHeld(m_unused_i2p_sessions_mutex);
std::string strDest;
{
LOCK(m_addr_fetches_mutex);
@@ -1578,6 +1596,7 @@ int CConnman::GetExtraBlockRelayCount() const
void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
{
+ AssertLockNotHeld(m_unused_i2p_sessions_mutex);
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_OPEN_CONNECTION);
FastRandomContext rng;
// Connect to specific addresses
@@ -1929,6 +1948,7 @@ std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo() const
void CConnman::ThreadOpenAddedConnections()
{
+ AssertLockNotHeld(m_unused_i2p_sessions_mutex);
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_ADD_CONNECTION);
while (true)
{
@@ -1958,6 +1978,7 @@ void CConnman::ThreadOpenAddedConnections()
// if successful, this moves the passed grant to the constructed node
void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant *grantOutbound, const char *pszDest, ConnectionType conn_type)
{
+ AssertLockNotHeld(m_unused_i2p_sessions_mutex);
assert(conn_type != ConnectionType::INBOUND);
//
diff --git a/src/net.h b/src/net.h
index 044701a339..73856c11ee 100644
--- a/src/net.h
+++ b/src/net.h
@@ -38,6 +38,7 @@
#include <map>
#include <memory>
#include <optional>
+#include <queue>
#include <thread>
#include <vector>
@@ -744,7 +745,7 @@ public:
bool GetNetworkActive() const { return fNetworkActive; };
bool GetUseAddrmanOutgoing() const { return m_use_addrman_outgoing; };
void SetNetworkActive(bool active);
- void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant* grantOutbound, const char* strDest, ConnectionType conn_type);
+ void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant* grantOutbound, const char* strDest, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
bool CheckIncomingNonce(uint64_t nonce);
bool ForNode(NodeId id, std::function<bool(CNode* pnode)> func);
@@ -820,7 +821,7 @@ public:
* - Max total outbound connection capacity filled
* - Max connection capacity for type is filled
*/
- bool AddConnection(const std::string& address, ConnectionType conn_type);
+ bool AddConnection(const std::string& address, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
size_t GetNodeCount(ConnectionDirection) const;
void GetNodeStats(std::vector<CNodeStats>& vstats) const;
@@ -886,10 +887,10 @@ private:
bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions);
bool InitBinds(const Options& options);
- void ThreadOpenAddedConnections() EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
+ void ThreadOpenAddedConnections() EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !m_unused_i2p_sessions_mutex);
void AddAddrFetch(const std::string& strDest) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex);
- void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex);
- void ThreadOpenConnections(std::vector<std::string> connect) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex);
+ void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_unused_i2p_sessions_mutex);
+ void ThreadOpenConnections(std::vector<std::string> connect) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex);
void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc);
void ThreadI2PAcceptIncoming();
void AcceptConnection(const ListenSocket& hListenSocket);
@@ -956,7 +957,7 @@ private:
bool AlreadyConnectedToAddress(const CAddress& addr);
bool AttemptToEvictConnection();
- CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type);
+ CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
void AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const;
void DeleteNode(CNode* pnode);
@@ -1128,6 +1129,26 @@ private:
std::vector<CService> m_onion_binds;
/**
+ * Mutex protecting m_i2p_sam_sessions.
+ */
+ Mutex m_unused_i2p_sessions_mutex;
+
+ /**
+ * A pool of created I2P SAM transient sessions that should be used instead
+ * of creating new ones in order to reduce the load on the I2P network.
+ * Creating a session in I2P is not cheap, thus if this is not empty, then
+ * pick an entry from it instead of creating a new session. If connecting to
+ * a host fails, then the created session is put to this pool for reuse.
+ */
+ std::queue<std::unique_ptr<i2p::sam::Session>> m_unused_i2p_sessions GUARDED_BY(m_unused_i2p_sessions_mutex);
+
+ /**
+ * Cap on the size of `m_unused_i2p_sessions`, to ensure it does not
+ * unexpectedly use too much memory.
+ */
+ static constexpr size_t MAX_UNUSED_I2P_SESSIONS_SIZE{10};
+
+ /**
* RAII helper to atomically create a copy of `m_nodes` and add a reference
* to each of the nodes. The nodes are released when this object is destroyed.
*/
diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp
index 85a3c36f39..d47afa9874 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 56f45907db..e171ddf06a 100644
--- a/src/qt/overviewpage.h
+++ b/src/qt/overviewpage.h
@@ -60,6 +60,7 @@ private:
std::unique_ptr<TransactionFilterProxy> 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 3be7e1a969..33ca74231f 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 fd9be52842..9b43981bc2 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;
};
diff --git a/src/support/lockedpool.cpp b/src/support/lockedpool.cpp
index e48accf0a4..ea2fbd706a 100644
--- a/src/support/lockedpool.cpp
+++ b/src/support/lockedpool.cpp
@@ -19,6 +19,9 @@
#endif
#include <algorithm>
+#include <limits>
+#include <stdexcept>
+#include <utility>
#ifdef ARENA_DEBUG
#include <iomanip>
#include <iostream>
diff --git a/src/support/lockedpool.h b/src/support/lockedpool.h
index 03e4e371a3..66fbc218ab 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 <stdint.h>
+#include <cstddef>
#include <list>
#include <map>
-#include <mutex>
#include <memory>
+#include <mutex>
#include <unordered_map>
/**
diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp
index 0925e2e9ee..e30ae845ef 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()
diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp
index 971814e9cd..a2ae078343 100644
--- a/src/wallet/rpc/wallet.cpp
+++ b/src/wallet/rpc/wallet.cpp
@@ -709,9 +709,12 @@ static RPCHelpMan migratewallet()
"A new wallet backup will need to be made.\n"
"\nThe migration process will create a backup of the wallet before migrating. This backup\n"
"file will be named <wallet name>-<timestamp>.legacy.bak and can be found in the directory\n"
- "for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet." +
- HELP_REQUIRING_PASSPHRASE,
- {},
+ "for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet."
+ "\nEncrypted wallets must have the passphrase provided as an argument to this call.",
+ {
+ {"wallet_name", RPCArg::Type::STR, RPCArg::DefaultHint{"the wallet name from the RPC endpoint"}, "The name of the wallet to migrate. If provided both here and in the RPC endpoint, the two must be identical."},
+ {"passphrase", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The wallet passphrase"},
+ },
RPCResult{
RPCResult::Type::OBJ, "", "",
{
@@ -727,16 +730,26 @@ static RPCHelpMan migratewallet()
},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
- std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request);
- if (!wallet) return NullUniValue;
+ std::string wallet_name;
+ if (GetWalletNameFromJSONRPCRequest(request, wallet_name)) {
+ if (!(request.params[0].isNull() || request.params[0].get_str() == wallet_name)) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "RPC endpoint wallet and wallet_name parameter specify different wallets");
+ }
+ } else {
+ if (request.params[0].isNull()) {
+ throw JSONRPCError(RPC_INVALID_PARAMETER, "Either RPC endpoint wallet or wallet_name parameter must be provided");
+ }
+ wallet_name = request.params[0].get_str();
+ }
- if (wallet->IsCrypted()) {
- throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: migratewallet on encrypted wallets is currently unsupported.");
+ SecureString wallet_pass;
+ wallet_pass.reserve(100);
+ if (!request.params[1].isNull()) {
+ wallet_pass = std::string_view{request.params[1].get_str()};
}
WalletContext& context = EnsureWalletContext(request.context);
-
- util::Result<MigrationResult> res = MigrateLegacyToDescriptor(std::move(wallet), context);
+ util::Result<MigrationResult> res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context);
if (!res) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
}
diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
index f534e10799..1807a98db1 100644
--- a/src/wallet/spend.cpp
+++ b/src/wallet/spend.cpp
@@ -1074,6 +1074,13 @@ util::Result<CreatedTransactionResult> CreateTransaction(
TRACE1(coin_selection, attempting_aps_create_tx, wallet.GetName().c_str());
CCoinControl tmp_cc = coin_control;
tmp_cc.m_avoid_partial_spends = true;
+
+ // Re-use the change destination from the first creation attempt to avoid skipping BIP44 indexes
+ const int ungrouped_change_pos = txr_ungrouped.change_pos;
+ if (ungrouped_change_pos != -1) {
+ ExtractDestination(txr_ungrouped.tx->vout[ungrouped_change_pos].scriptPubKey, tmp_cc.destChange);
+ }
+
auto txr_grouped = CreateTransactionInternal(wallet, vecSend, change_pos, tmp_cc, sign);
// if fee of this alternative one is within the range of the max fee, we use this one
const bool use_aps{txr_grouped.has_value() ? (txr_grouped->fee <= txr_ungrouped.fee + wallet.m_max_aps_fee) : false};
diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
index 36fe32e54d..b957055c0f 100644
--- a/src/wallet/wallet.cpp
+++ b/src/wallet/wallet.cpp
@@ -25,6 +25,7 @@
#include <script/descriptor.h>
#include <script/script.h>
#include <script/signingprovider.h>
+#include <support/cleanse.h>
#include <txmempool.h>
#include <util/bip32.h>
#include <util/check.h>
@@ -589,8 +590,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;
}
}
@@ -3102,6 +3102,24 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& 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<int64_t> 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<CWallet>& 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<int64_t> 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)) {
@@ -3287,7 +3294,10 @@ bool CWallet::Lock()
{
LOCK(cs_wallet);
- vMasterKey.clear();
+ if (!vMasterKey.empty()) {
+ memory_cleanse(vMasterKey.data(), vMasterKey.size() * sizeof(decltype(vMasterKey)::value_type));
+ vMasterKey.clear();
+ }
}
NotifyStatusChanged(this);
@@ -3757,7 +3767,7 @@ std::optional<MigrationData> CWallet::GetDescriptorsForLegacy(bilingual_str& err
std::optional<MigrationData> res = legacy_spkm->MigrateToDescriptor();
if (res == std::nullopt) {
- error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure the wallet is unlocked first");
+ error = _("Error: Unable to produce descriptors for this legacy wallet. Make sure to provide the wallet's passphrase if it is encrypted.");
return std::nullopt;
}
return res;
@@ -3918,6 +3928,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) {
@@ -4030,27 +4057,19 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
return true;
}
-util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context)
+util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context)
{
MigrationResult res;
bilingual_str error;
std::vector<bilingual_str> warnings;
- // Make a backup of the DB
- std::string wallet_name = wallet->GetName();
- fs::path this_wallet_dir = fs::absolute(fs::PathFromString(wallet->GetDatabase().Filename())).parent_path();
- fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
- fs::path backup_path = this_wallet_dir / backup_filename;
- if (!wallet->BackupWallet(fs::PathToString(backup_path))) {
- return util::Error{_("Error: Unable to make a backup of your wallet")};
- }
- res.backup_path = backup_path;
-
- // Unload the wallet so that nothing else tries to use it while we're changing it
- if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
- return util::Error{_("Unable to unload the wallet before migrating")};
+ // If the wallet is still loaded, unload it so that nothing else tries to use it while we're changing it
+ if (auto wallet = GetWallet(context, wallet_name)) {
+ if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
+ return util::Error{_("Unable to unload the wallet before migrating")};
+ }
+ UnloadWallet(std::move(wallet));
}
- UnloadWallet(std::move(wallet));
// Load the wallet but only in the context of this function.
// No signals should be connected nor should anything else be aware of this wallet
@@ -4064,15 +4083,43 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
return util::Error{Untranslated("Wallet file verification failed.") + Untranslated(" ") + error};
}
+ // Make the local wallet
std::shared_ptr<CWallet> local_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings);
if (!local_wallet) {
return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error};
}
+ // Before anything else, check if there is something to migrate.
+ if (!local_wallet->GetLegacyScriptPubKeyMan()) {
+ return util::Error{_("Error: This wallet is already a descriptor wallet")};
+ }
+
+ // Make a backup of the DB
+ fs::path this_wallet_dir = fs::absolute(fs::PathFromString(local_wallet->GetDatabase().Filename())).parent_path();
+ fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime()));
+ fs::path backup_path = this_wallet_dir / backup_filename;
+ if (!local_wallet->BackupWallet(fs::PathToString(backup_path))) {
+ return util::Error{_("Error: Unable to make a backup of your wallet")};
+ }
+ res.backup_path = backup_path;
+
bool success = false;
{
LOCK(local_wallet->cs_wallet);
+ // Unlock the wallet if needed
+ if (local_wallet->IsLocked() && !local_wallet->Unlock(passphrase)) {
+ if (passphrase.find('\0') == std::string::npos) {
+ return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect.")};
+ } else {
+ return util::Error{Untranslated("Error: Wallet decryption failed, the wallet passphrase entered was incorrect. "
+ "The passphrase contains a null character (ie - a zero byte). "
+ "If this passphrase was set with a version of this software prior to 25.0, "
+ "please try again with only the characters up to — but not including — "
+ "the first null character.")};
+ }
+ }
+
// First change to using SQLite
if (!local_wallet->MigrateToSQLite(error)) return util::Error{error};
diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
index 2f10c598b5..637e9e6d41 100644
--- a/src/wallet/wallet.h
+++ b/src/wallet/wallet.h
@@ -1006,7 +1006,7 @@ struct MigrationResult {
};
//! Do all steps to migrate a legacy wallet to a descriptor wallet
-util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>&& wallet, WalletContext& context);
+util::Result<MigrationResult> MigrateLegacyToDescriptor(const std::string& wallet_name, const SecureString& passphrase, WalletContext& context);
} // namespace wallet
#endif // BITCOIN_WALLET_WALLET_H
diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py
index caa4af957a..b9233ef2cb 100755
--- a/test/functional/test_runner.py
+++ b/test/functional/test_runner.py
@@ -199,6 +199,8 @@ BASE_SCRIPTS = [
'rpc_blockchain.py',
'rpc_deprecated.py',
'wallet_disable.py',
+ 'wallet_change_address.py --legacy-wallet',
+ 'wallet_change_address.py --descriptors',
'p2p_addr_relay.py',
'p2p_getaddr_caching.py',
'p2p_getdata.py',
diff --git a/test/functional/wallet_bumpfee.py b/test/functional/wallet_bumpfee.py
index f4ae697292..016992dbea 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')
diff --git a/test/functional/wallet_change_address.py b/test/functional/wallet_change_address.py
new file mode 100755
index 0000000000..1c0dd09c82
--- /dev/null
+++ b/test/functional/wallet_change_address.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# Copyright (c) 2023 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test wallet change address selection"""
+
+import re
+
+from test_framework.blocktools import COINBASE_MATURITY
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import (
+ assert_equal,
+)
+
+
+class WalletChangeAddressTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.setup_clean_chain = True
+ self.num_nodes = 3
+ # discardfee is used to make change outputs less likely in the change_pos test
+ self.extra_args = [
+ [],
+ ["-discardfee=1"],
+ ["-avoidpartialspends", "-discardfee=1"]
+ ]
+
+ def skip_test_if_missing_module(self):
+ self.skip_if_no_wallet()
+
+ def assert_change_index(self, node, tx, index):
+ change_index = None
+ for vout in tx["vout"]:
+ info = node.getaddressinfo(vout["scriptPubKey"]["address"])
+ if (info["ismine"] and info["ischange"]):
+ change_index = int(re.findall(r'\d+', info["hdkeypath"])[-1])
+ break
+ assert_equal(change_index, index)
+
+ def assert_change_pos(self, wallet, tx, pos):
+ change_pos = None
+ for index, output in enumerate(tx["vout"]):
+ info = wallet.getaddressinfo(output["scriptPubKey"]["address"])
+ if (info["ismine"] and info["ischange"]):
+ change_pos = index
+ break
+ assert_equal(change_pos, pos)
+
+ def run_test(self):
+ self.log.info("Setting up")
+ # Mine some coins
+ self.generate(self.nodes[0], COINBASE_MATURITY + 1)
+
+ # Get some addresses from the two nodes
+ addr1 = [self.nodes[1].getnewaddress() for _ in range(3)]
+ addr2 = [self.nodes[2].getnewaddress() for _ in range(3)]
+ addrs = addr1 + addr2
+
+ # Send 1 + 0.5 coin to each address
+ [self.nodes[0].sendtoaddress(addr, 1.0) for addr in addrs]
+ [self.nodes[0].sendtoaddress(addr, 0.5) for addr in addrs]
+ self.generate(self.nodes[0], 1)
+
+ for i in range(20):
+ for n in [1, 2]:
+ self.log.debug(f"Send transaction from node {n}: expected change index {i}")
+ txid = self.nodes[n].sendtoaddress(self.nodes[0].getnewaddress(), 0.2)
+ tx = self.nodes[n].getrawtransaction(txid, True)
+ # find the change output and ensure that expected change index was used
+ self.assert_change_index(self.nodes[n], tx, i)
+
+ # Start next test with fresh wallets and new coins
+ self.nodes[1].createwallet("w1")
+ self.nodes[2].createwallet("w2")
+ w1 = self.nodes[1].get_wallet_rpc("w1")
+ w2 = self.nodes[2].get_wallet_rpc("w2")
+ addr1 = w1.getnewaddress()
+ addr2 = w2.getnewaddress()
+ self.nodes[0].sendtoaddress(addr1, 3.0)
+ self.nodes[0].sendtoaddress(addr1, 0.1)
+ self.nodes[0].sendtoaddress(addr2, 3.0)
+ self.nodes[0].sendtoaddress(addr2, 0.1)
+ self.generate(self.nodes[0], 1)
+
+ sendTo1 = self.nodes[0].getnewaddress()
+ sendTo2 = self.nodes[0].getnewaddress()
+ sendTo3 = self.nodes[0].getnewaddress()
+
+ # The avoid partial spends wallet will always create a change output
+ node = self.nodes[2]
+ res = w2.send(outputs=[{sendTo1: 1.0}, {sendTo2: 1.0}, {sendTo3: 0.9999}], options={"change_position": 0})
+ tx = node.getrawtransaction(res["txid"], True)
+ self.assert_change_pos(w2, tx, 0)
+
+ # The default wallet will internally create a tx without change first,
+ # then create a second candidate using APS that requires a change output.
+ # Ensure that the user-configured change position is kept
+ node = self.nodes[1]
+ res = w1.send(outputs=[{sendTo1: 1.0}, {sendTo2: 1.0}, {sendTo3: 0.9999}], options={"change_position": 0})
+ tx = node.getrawtransaction(res["txid"], True)
+ # If the wallet ignores the user's change_position there is still a 25%
+ # that the random change position passes the test
+ self.assert_change_pos(w1, tx, 0)
+
+if __name__ == '__main__':
+ WalletChangeAddressTest().main()
diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
index 9744009af8..ffc3c51bbf 100755
--- a/test/functional/wallet_importdescriptors.py
+++ b/test/functional/wallet_importdescriptors.py
@@ -447,14 +447,14 @@ class ImportDescriptorsTest(BitcoinTestFramework):
wallet=wmulti_priv)
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1001) # Range end (1000) is inclusive, so 1001 addresses generated
- addr = wmulti_priv.getnewaddress('', 'bech32')
+ addr = wmulti_priv.getnewaddress('', 'bech32') # uses receive 0
assert_equal(addr, 'bcrt1qdt0qy5p7dzhxzmegnn4ulzhard33s2809arjqgjndx87rv5vd0fq2czhy8') # Derived at m/84'/0'/0'/0
- change_addr = wmulti_priv.getrawchangeaddress('bech32')
- assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e')
+ change_addr = wmulti_priv.getrawchangeaddress('bech32') # uses change 0
+ assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e') # Derived at m/84'/1'/0'/0
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000)
txid = w0.sendtoaddress(addr, 10)
self.generate(self.nodes[0], 6)
- send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8)
+ send_txid = wmulti_priv.sendtoaddress(w0.getnewaddress(), 8) # uses change 1
decoded = wmulti_priv.gettransaction(txid=send_txid, verbose=True)['decoded']
assert_equal(len(decoded['vin'][0]['txinwitness']), 4)
self.sync_all()
@@ -480,12 +480,12 @@ class ImportDescriptorsTest(BitcoinTestFramework):
wallet=wmulti_pub)
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 1000) # The first one was already consumed by previous import and is detected as used
- addr = wmulti_pub.getnewaddress('', 'bech32')
+ addr = wmulti_pub.getnewaddress('', 'bech32') # uses receive 1
assert_equal(addr, 'bcrt1qp8s25ckjl7gr6x2q3dx3tn2pytwp05upkjztk6ey857tt50r5aeqn6mvr9') # Derived at m/84'/0'/0'/1
- change_addr = wmulti_pub.getrawchangeaddress('bech32')
- assert_equal(change_addr, 'bcrt1qzxl0qz2t88kljdnkzg4n4gapr6kte26390gttrg79x66nt4p04fssj53nl')
- assert(send_txid in self.nodes[0].getrawmempool(True))
- assert(send_txid in (x['txid'] for x in wmulti_pub.listunspent(0)))
+ change_addr = wmulti_pub.getrawchangeaddress('bech32') # uses change 2
+ assert_equal(change_addr, 'bcrt1qp6j3jw8yetefte7kw6v5pc89rkgakzy98p6gf7ayslaveaxqyjusnw580c') # Derived at m/84'/1'/0'/2
+ assert send_txid in self.nodes[0].getrawmempool(True)
+ assert send_txid in (x['txid'] for x in wmulti_pub.listunspent(0))
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999)
# generate some utxos for next tests
diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py
index 4f060f9960..e05752d0e7 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)
@@ -396,11 +403,75 @@ class WalletMigrationTest(BitcoinTestFramework):
def test_encrypted(self):
self.log.info("Test migration of an encrypted wallet")
wallet = self.create_legacy_wallet("encrypted")
+ default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
wallet.encryptwallet("pass")
+ addr = wallet.getnewaddress()
+ txid = default.sendtoaddress(addr, 1)
+ self.generate(self.nodes[0], 1)
+ bals = wallet.getbalances()
+
+ assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet)
+ assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet, None, "badpass")
+ assert_raises_rpc_error(-4, "The passphrase contains a null character", wallet.migratewallet, None, "pass\0with\0null")
+
+ wallet.migratewallet(passphrase="pass")
+
+ info = wallet.getwalletinfo()
+ assert_equal(info["descriptors"], True)
+ assert_equal(info["format"], "sqlite")
+ assert_equal(info["unlocked_until"], 0)
+ wallet.gettransaction(txid)
+
+ assert_equal(bals, wallet.getbalances())
+
+ def test_unloaded(self):
+ self.log.info("Test migration of a wallet that isn't loaded")
+ wallet = self.create_legacy_wallet("notloaded")
+ default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+
+ addr = wallet.getnewaddress()
+ txid = default.sendtoaddress(addr, 1)
+ self.generate(self.nodes[0], 1)
+ bals = wallet.getbalances()
+
+ wallet.unloadwallet()
- assert_raises_rpc_error(-15, "Error: migratewallet on encrypted wallets is currently unsupported.", wallet.migratewallet)
- # TODO: Fix migratewallet so that we can actually migrate encrypted wallets
+ assert_raises_rpc_error(-8, "RPC endpoint wallet and wallet_name parameter specify different wallets", wallet.migratewallet, "someotherwallet")
+ assert_raises_rpc_error(-8, "Either RPC endpoint wallet or wallet_name parameter must be provided", self.nodes[0].migratewallet)
+ self.nodes[0].migratewallet("notloaded")
+
+ info = wallet.getwalletinfo()
+ assert_equal(info["descriptors"], True)
+ assert_equal(info["format"], "sqlite")
+ wallet.gettransaction(txid)
+
+ assert_equal(bals, wallet.getbalances())
+
+ def test_unloaded_by_path(self):
+ self.log.info("Test migration of a wallet that isn't loaded, specified by path")
+ wallet = self.create_legacy_wallet("notloaded2")
+ default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
+
+ addr = wallet.getnewaddress()
+ txid = default.sendtoaddress(addr, 1)
+ self.generate(self.nodes[0], 1)
+ bals = wallet.getbalances()
+
+ wallet.unloadwallet()
+
+ wallet_file_path = os.path.join(self.nodes[0].datadir, "regtest", "wallets", "notloaded2")
+ self.nodes[0].migratewallet(wallet_file_path)
+
+ # Because we gave the name by full path, the loaded wallet's name is that path too.
+ wallet = self.nodes[0].get_wallet_rpc(wallet_file_path)
+
+ info = wallet.getwalletinfo()
+ assert_equal(info["descriptors"], True)
+ assert_equal(info["format"], "sqlite")
+ wallet.gettransaction(txid)
+
+ assert_equal(bals, wallet.getbalances())
def run_test(self):
self.generate(self.nodes[0], 101)
@@ -412,6 +483,8 @@ class WalletMigrationTest(BitcoinTestFramework):
self.test_no_privkeys()
self.test_pk_coinbases()
self.test_encrypted()
+ self.test_unloaded()
+ self.test_unloaded_by_path()
if __name__ == '__main__':
WalletMigrationTest().main()