Merge bitcoin/bitcoin#26878: [24.x] Backports

784a754aa4 wallet, rpc: Update migratewallet help text for encrypted wallets (Andrew Chow)
debcfe313a tests: Tests for migrating wallets by name, and providing passphrase (Andrew Chow)
ccc72fecd7 wallet: Be able to unlock the wallet for migration (Andrew Chow)
50dd8b13df rpc: Allow users to specify wallet name for migratewallet (Andrew Chow)
648b06256d wallet: Allow MigrateLegacyToDescriptor to take a wallet name (Andrew Chow)
ab3bd457cd i2p: use consistent number of tunnels with i2pd and Java I2P (Vasil Dimov)
29cdf42226 i2p: lower the number of tunnels for transient sessions (Vasil Dimov)
5027e93b6a i2p: reuse created I2P sessions if not used (Vasil Dimov)
a62c541ae8 wallet: reuse change dest when recreating TX with avoidpartialspends (Matthew Zipkin)
64e7db6f4f Zero out wallet master key upon lock (John Moffett)
b7e242ecb3 Correctly limit overview transaction list (John Moffett)
cff67180b3 depends: fix systemtap download URL (fanquake)
7cf73dfed5 Add missing includes to fix gcc-13 compile error (MarcoFalke)
07397cdede addrdb: Only call Serialize() once (Martin Zumsande)
91f83dbeb1 hash: add HashedSourceWriter (Martin Zumsande)
5c824ac5e1 For feebump, ignore abandoned descendant spends (John Moffett)
428dcd51e6 wallet: Skip rescanning if wallet is more recent than tip (Andrew Chow)
cbcdafa471 test: wallet: check that labels are migrated to watchonly wallet (Sebastian Falbesoner)
342abfb3f4 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 784a754aa4
  achow101:
    ACK 784a754aa4
  hebasto:
    ACK 784a754aa4, I've made backporting locally and got a diff between my branch and this PR as follows:

Tree-SHA512: 8ea84aa02d7907ff1e202e1302b441ce9ed2198bf383620ad40056a5d7e8ea88e1047abef0b92d85648016bf9b3195c974be3806ccebd85bef4f85c326869e43
This commit is contained in:
glozow 2023-02-27 17:53:20 +00:00
commit c8c85ca16e
No known key found for this signature in database
GPG key ID: BA03F4DBE0C63FB4
23 changed files with 447 additions and 95 deletions

View file

@ -1,6 +1,6 @@
package=systemtap package=systemtap
$(package)_version=4.7 $(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)_file_name=$(package)-$($(package)_version).tar.gz
$(package)_sha256_hash=43a0a3db91aa4d41e28015b39a65e62059551f3cc7377ebf3a3a5ca7339e7b1f $(package)_sha256_hash=43a0a3db91aa4d41e28015b39a65e62059551f3cc7377ebf3a3a5ca7339e7b1f
$(package)_patches=remove_SDT_ASM_SECTION_AUTOGROUP_SUPPORT_check.patch $(package)_patches=remove_SDT_ASM_SECTION_AUTOGROUP_SUPPORT_check.patch

View file

@ -34,10 +34,9 @@ bool SerializeDB(Stream& stream, const Data& data)
{ {
// Write and commit header, data // Write and commit header, data
try { try {
CHashWriter hasher(stream.GetType(), stream.GetVersion()); HashedSourceWriter hashwriter{stream};
stream << Params().MessageStart() << data; hashwriter << Params().MessageStart() << data;
hasher << Params().MessageStart() << data; stream << hashwriter.GetHash();
stream << hasher.GetHash();
} catch (const std::exception& e) { } catch (const std::exception& e) {
return error("%s: Serialize or I/O error - %s", __func__, e.what()); return error("%s: Serialize or I/O error - %s", __func__, e.what());
} }

View file

@ -1178,8 +1178,7 @@ void AddrMan::Unserialize(Stream& s_)
} }
// explicit instantiation // explicit instantiation
template void AddrMan::Serialize(CHashWriter& s) const; template void AddrMan::Serialize(HashedSourceWriter<CAutoFile>& s) const;
template void AddrMan::Serialize(CAutoFile& s) const;
template void AddrMan::Serialize(CDataStream& s) const; template void AddrMan::Serialize(CDataStream& s) const;
template void AddrMan::Unserialize(CAutoFile& s); template void AddrMan::Unserialize(CAutoFile& s);
template void AddrMan::Unserialize(CHashVerifier<CAutoFile>& s); template void AddrMan::Unserialize(CHashVerifier<CAutoFile>& s);

View file

@ -6,6 +6,7 @@
#ifndef BITCOIN_HASH_H #ifndef BITCOIN_HASH_H
#define BITCOIN_HASH_H #define BITCOIN_HASH_H
#include <attributes.h>
#include <crypto/common.h> #include <crypto/common.h>
#include <crypto/ripemd160.h> #include <crypto/ripemd160.h>
#include <crypto/sha256.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. */ /** Compute the 256-bit hash of an object's serialization. */
template<typename T> template<typename T>
uint256 SerializeHash(const T& obj, int nType=SER_GETHASH, int nVersion=PROTOCOL_VERSION) uint256 SerializeHash(const T& obj, int nType=SER_GETHASH, int nVersion=PROTOCOL_VERSION)

View file

@ -379,7 +379,9 @@ void Session::CreateIfNotCreatedAlready()
// in the reply in DESTINATION=. // in the reply in DESTINATION=.
const Reply& reply = SendRequestAndGetReply( const Reply& reply = SendRequestAndGetReply(
*sock, *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")); m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
} else { } else {
@ -395,7 +397,8 @@ void Session::CreateIfNotCreatedAlready()
const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key)); const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
SendRequestAndGetReply(*sock, 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, session_id,
private_key_b64)); private_key_b64));
} }

View file

@ -436,6 +436,7 @@ static CAddress GetBindAddress(const Sock& sock)
CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type) CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type)
{ {
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
assert(conn_type != ConnectionType::INBOUND); assert(conn_type != ConnectionType::INBOUND);
if (pszDest == nullptr) { if (pszDest == nullptr) {
@ -496,8 +497,23 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
if (m_i2p_sam_session) { if (m_i2p_sam_session) {
connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed); connected = m_i2p_sam_session->Connect(addrConnect, conn, proxyConnectionFailed);
} else { } 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); 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) { if (connected) {
@ -1048,6 +1064,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr<Sock>&& sock,
bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type) bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type)
{ {
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
std::optional<int> max_connections; std::optional<int> max_connections;
switch (conn_type) { switch (conn_type) {
case ConnectionType::INBOUND: case ConnectionType::INBOUND:
@ -1510,6 +1527,7 @@ void CConnman::DumpAddresses()
void CConnman::ProcessAddrFetch() void CConnman::ProcessAddrFetch()
{ {
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
std::string strDest; std::string strDest;
{ {
LOCK(m_addr_fetches_mutex); LOCK(m_addr_fetches_mutex);
@ -1578,6 +1596,7 @@ int CConnman::GetExtraBlockRelayCount() const
void CConnman::ThreadOpenConnections(const std::vector<std::string> connect) void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
{ {
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_OPEN_CONNECTION); SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_OPEN_CONNECTION);
FastRandomContext rng; FastRandomContext rng;
// Connect to specific addresses // Connect to specific addresses
@ -1929,6 +1948,7 @@ std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo() const
void CConnman::ThreadOpenAddedConnections() void CConnman::ThreadOpenAddedConnections()
{ {
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_ADD_CONNECTION); SetSyscallSandboxPolicy(SyscallSandboxPolicy::NET_ADD_CONNECTION);
while (true) while (true)
{ {
@ -1958,6 +1978,7 @@ void CConnman::ThreadOpenAddedConnections()
// if successful, this moves the passed grant to the constructed node // 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) 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); assert(conn_type != ConnectionType::INBOUND);
// //

View file

@ -38,6 +38,7 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <queue>
#include <thread> #include <thread>
#include <vector> #include <vector>
@ -744,7 +745,7 @@ public:
bool GetNetworkActive() const { return fNetworkActive; }; bool GetNetworkActive() const { return fNetworkActive; };
bool GetUseAddrmanOutgoing() const { return m_use_addrman_outgoing; }; bool GetUseAddrmanOutgoing() const { return m_use_addrman_outgoing; };
void SetNetworkActive(bool active); 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 CheckIncomingNonce(uint64_t nonce);
bool ForNode(NodeId id, std::function<bool(CNode* pnode)> func); bool ForNode(NodeId id, std::function<bool(CNode* pnode)> func);
@ -820,7 +821,7 @@ public:
* - Max total outbound connection capacity filled * - Max total outbound connection capacity filled
* - Max connection capacity for type is 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; size_t GetNodeCount(ConnectionDirection) const;
void GetNodeStats(std::vector<CNodeStats>& vstats) const; void GetNodeStats(std::vector<CNodeStats>& vstats) const;
@ -886,10 +887,10 @@ private:
bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions); bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions);
bool InitBinds(const Options& options); 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 AddAddrFetch(const std::string& strDest) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex);
void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_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); 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 ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc);
void ThreadI2PAcceptIncoming(); void ThreadI2PAcceptIncoming();
void AcceptConnection(const ListenSocket& hListenSocket); void AcceptConnection(const ListenSocket& hListenSocket);
@ -956,7 +957,7 @@ private:
bool AlreadyConnectedToAddress(const CAddress& addr); bool AlreadyConnectedToAddress(const CAddress& addr);
bool AttemptToEvictConnection(); 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 AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr) const;
void DeleteNode(CNode* pnode); void DeleteNode(CNode* pnode);
@ -1127,6 +1128,26 @@ private:
*/ */
std::vector<CService> m_onion_binds; 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 * 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. * to each of the nodes. The nodes are released when this object is destroyed.

View file

@ -264,7 +264,6 @@ void OverviewPage::setWalletModel(WalletModel *model)
// Set up transaction list // Set up transaction list
filter.reset(new TransactionFilterProxy()); filter.reset(new TransactionFilterProxy());
filter->setSourceModel(model->getTransactionTableModel()); filter->setSourceModel(model->getTransactionTableModel());
filter->setLimit(NUM_ITEMS);
filter->setDynamicSortFilter(true); filter->setDynamicSortFilter(true);
filter->setSortRole(Qt::EditRole); filter->setSortRole(Qt::EditRole);
filter->setShowInactive(false); filter->setShowInactive(false);
@ -273,6 +272,10 @@ void OverviewPage::setWalletModel(WalletModel *model)
ui->listTransactions->setModel(filter.get()); ui->listTransactions->setModel(filter.get());
ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress); 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 // Keep up to date with wallet
setBalance(model->getCachedBalance()); setBalance(model->getCachedBalance());
connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance); connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance);
@ -301,6 +304,16 @@ void OverviewPage::changeEvent(QEvent* e)
QWidget::changeEvent(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() void OverviewPage::updateDisplayUnit()
{ {
if (walletModel && walletModel->getOptionsModel()) { if (walletModel && walletModel->getOptionsModel()) {

View file

@ -60,6 +60,7 @@ private:
std::unique_ptr<TransactionFilterProxy> filter; std::unique_ptr<TransactionFilterProxy> filter;
private Q_SLOTS: private Q_SLOTS:
void LimitTransactionRows();
void updateDisplayUnit(); void updateDisplayUnit();
void handleTransactionClicked(const QModelIndex &index); void handleTransactionClicked(const QModelIndex &index);
void updateAlerts(const QString &warnings); void updateAlerts(const QString &warnings);

View file

@ -17,7 +17,6 @@ TransactionFilterProxy::TransactionFilterProxy(QObject *parent) :
typeFilter(ALL_TYPES), typeFilter(ALL_TYPES),
watchOnlyFilter(WatchOnlyFilter_All), watchOnlyFilter(WatchOnlyFilter_All),
minAmount(0), minAmount(0),
limitRows(-1),
showInactive(true) showInactive(true)
{ {
} }
@ -92,25 +91,8 @@ void TransactionFilterProxy::setWatchOnlyFilter(WatchOnlyFilter filter)
invalidateFilter(); invalidateFilter();
} }
void TransactionFilterProxy::setLimit(int limit)
{
this->limitRows = limit;
}
void TransactionFilterProxy::setShowInactive(bool _showInactive) void TransactionFilterProxy::setShowInactive(bool _showInactive)
{ {
this->showInactive = _showInactive; this->showInactive = _showInactive;
invalidateFilter(); invalidateFilter();
} }
int TransactionFilterProxy::rowCount(const QModelIndex &parent) const
{
if(limitRows != -1)
{
return std::min(QSortFilterProxyModel::rowCount(parent), limitRows);
}
else
{
return QSortFilterProxyModel::rowCount(parent);
}
}

View file

@ -42,14 +42,9 @@ public:
void setMinAmount(const CAmount& minimum); void setMinAmount(const CAmount& minimum);
void setWatchOnlyFilter(WatchOnlyFilter filter); void setWatchOnlyFilter(WatchOnlyFilter filter);
/** Set maximum number of rows returned, -1 if unlimited. */
void setLimit(int limit);
/** Set whether to show conflicted transactions. */ /** Set whether to show conflicted transactions. */
void setShowInactive(bool showInactive); void setShowInactive(bool showInactive);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
protected: protected:
bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override;
@ -60,7 +55,6 @@ private:
quint32 typeFilter; quint32 typeFilter;
WatchOnlyFilter watchOnlyFilter; WatchOnlyFilter watchOnlyFilter;
CAmount minAmount; CAmount minAmount;
int limitRows;
bool showInactive; bool showInactive;
}; };

View file

@ -19,6 +19,9 @@
#endif #endif
#include <algorithm> #include <algorithm>
#include <limits>
#include <stdexcept>
#include <utility>
#ifdef ARENA_DEBUG #ifdef ARENA_DEBUG
#include <iomanip> #include <iomanip>
#include <iostream> #include <iostream>

View file

@ -5,11 +5,11 @@
#ifndef BITCOIN_SUPPORT_LOCKEDPOOL_H #ifndef BITCOIN_SUPPORT_LOCKEDPOOL_H
#define BITCOIN_SUPPORT_LOCKEDPOOL_H #define BITCOIN_SUPPORT_LOCKEDPOOL_H
#include <stdint.h> #include <cstddef>
#include <list> #include <list>
#include <map> #include <map>
#include <mutex>
#include <memory> #include <memory>
#include <mutex>
#include <unordered_map> #include <unordered_map>
/** /**

View file

@ -441,4 +441,18 @@ BOOST_AUTO_TEST_CASE(streams_buffered_file_rand)
fs::remove(streams_test_filename); 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() BOOST_AUTO_TEST_SUITE_END()

View file

@ -709,9 +709,12 @@ static RPCHelpMan migratewallet()
"A new wallet backup will need to be made.\n" "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" "\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" "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." + "for this wallet. In the event of an incorrect migration, the backup can be restored using restorewallet."
HELP_REQUIRING_PASSPHRASE, "\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{
RPCResult::Type::OBJ, "", "", RPCResult::Type::OBJ, "", "",
{ {
@ -727,16 +730,26 @@ static RPCHelpMan migratewallet()
}, },
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{ {
std::shared_ptr<CWallet> wallet = GetWalletForJSONRPCRequest(request); std::string wallet_name;
if (!wallet) return NullUniValue; 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()) { SecureString wallet_pass;
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: migratewallet on encrypted wallets is currently unsupported."); wallet_pass.reserve(100);
if (!request.params[1].isNull()) {
wallet_pass = std::string_view{request.params[1].get_str()};
} }
WalletContext& context = EnsureWalletContext(request.context); WalletContext& context = EnsureWalletContext(request.context);
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(wallet_name, wallet_pass, context);
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(std::move(wallet), context);
if (!res) { if (!res) {
throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original); throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(res).original);
} }

View file

@ -1074,6 +1074,13 @@ util::Result<CreatedTransactionResult> CreateTransaction(
TRACE1(coin_selection, attempting_aps_create_tx, wallet.GetName().c_str()); TRACE1(coin_selection, attempting_aps_create_tx, wallet.GetName().c_str());
CCoinControl tmp_cc = coin_control; CCoinControl tmp_cc = coin_control;
tmp_cc.m_avoid_partial_spends = true; 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); 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 // 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}; const bool use_aps{txr_grouped.has_value() ? (txr_grouped->fee <= txr_ungrouped.fee + wallet.m_max_aps_fee) : false};

View file

@ -25,6 +25,7 @@
#include <script/descriptor.h> #include <script/descriptor.h>
#include <script/script.h> #include <script/script.h>
#include <script/signingprovider.h> #include <script/signingprovider.h>
#include <support/cleanse.h>
#include <txmempool.h> #include <txmempool.h>
#include <util/bip32.h> #include <util/bip32.h>
#include <util/check.h> #include <util/check.h>
@ -589,8 +590,7 @@ bool CWallet::HasWalletSpend(const CTransactionRef& tx) const
AssertLockHeld(cs_wallet); AssertLockHeld(cs_wallet);
const uint256& txid = tx->GetHash(); const uint256& txid = tx->GetHash();
for (unsigned int i = 0; i < tx->vout.size(); ++i) { for (unsigned int i = 0; i < tx->vout.size(); ++i) {
auto iter = mapTxSpends.find(COutPoint(txid, i)); if (IsSpent(COutPoint(txid, i))) {
if (iter != mapTxSpends.end()) {
return true; return true;
} }
} }
@ -3102,6 +3102,24 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
if (tip_height && *tip_height != rescan_height) 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 // 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 // `while` loop below can make startup very slow, so only check blocks on disk
// if necessary. // if necessary.
@ -3134,17 +3152,6 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
chain.initMessage(_("Rescanning…").translated); chain.initMessage(_("Rescanning…").translated);
walletInstance->WalletLogPrintf("Rescanning last %i blocks (from block %i)...\n", *tip_height - rescan_height, rescan_height); 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); 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)) { 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); LOCK(cs_wallet);
vMasterKey.clear(); if (!vMasterKey.empty()) {
memory_cleanse(vMasterKey.data(), vMasterKey.size() * sizeof(decltype(vMasterKey)::value_type));
vMasterKey.clear();
}
} }
NotifyStatusChanged(this); NotifyStatusChanged(this);
@ -3757,7 +3767,7 @@ std::optional<MigrationData> CWallet::GetDescriptorsForLegacy(bilingual_str& err
std::optional<MigrationData> res = legacy_spkm->MigrateToDescriptor(); std::optional<MigrationData> res = legacy_spkm->MigrateToDescriptor();
if (res == std::nullopt) { 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 std::nullopt;
} }
return res; 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 // Remove the things to delete
if (dests_to_delete.size() > 0) { if (dests_to_delete.size() > 0) {
for (const auto& dest : dests_to_delete) { for (const auto& dest : dests_to_delete) {
@ -4030,27 +4057,19 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
return true; 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; MigrationResult res;
bilingual_str error; bilingual_str error;
std::vector<bilingual_str> warnings; std::vector<bilingual_str> warnings;
// Make a backup of the DB // If the wallet is still loaded, unload it so that nothing else tries to use it while we're changing it
std::string wallet_name = wallet->GetName(); if (auto wallet = GetWallet(context, wallet_name)) {
fs::path this_wallet_dir = fs::absolute(fs::PathFromString(wallet->GetDatabase().Filename())).parent_path(); if (!RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings)) {
fs::path backup_filename = fs::PathFromString(strprintf("%s-%d.legacy.bak", wallet_name, GetTime())); return util::Error{_("Unable to unload the wallet before migrating")};
fs::path backup_path = this_wallet_dir / backup_filename; }
if (!wallet->BackupWallet(fs::PathToString(backup_path))) { UnloadWallet(std::move(wallet));
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")};
}
UnloadWallet(std::move(wallet));
// Load the wallet but only in the context of this function. // 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 // 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}; 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); std::shared_ptr<CWallet> local_wallet = CWallet::Create(empty_context, wallet_name, std::move(database), options.create_flags, error, warnings);
if (!local_wallet) { if (!local_wallet) {
return util::Error{Untranslated("Wallet loading failed.") + Untranslated(" ") + error}; 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; bool success = false;
{ {
LOCK(local_wallet->cs_wallet); 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 // First change to using SQLite
if (!local_wallet->MigrateToSQLite(error)) return util::Error{error}; if (!local_wallet->MigrateToSQLite(error)) return util::Error{error};

View file

@ -1006,7 +1006,7 @@ struct MigrationResult {
}; };
//! Do all steps to migrate a legacy wallet to a descriptor wallet //! 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 } // namespace wallet
#endif // BITCOIN_WALLET_WALLET_H #endif // BITCOIN_WALLET_WALLET_H

View file

@ -199,6 +199,8 @@ BASE_SCRIPTS = [
'rpc_blockchain.py', 'rpc_blockchain.py',
'rpc_deprecated.py', 'rpc_deprecated.py',
'wallet_disable.py', 'wallet_disable.py',
'wallet_change_address.py --legacy-wallet',
'wallet_change_address.py --descriptors',
'p2p_addr_relay.py', 'p2p_addr_relay.py',
'p2p_getaddr_caching.py', 'p2p_getaddr_caching.py',
'p2p_getdata.py', 'p2p_getdata.py',

View file

@ -89,6 +89,7 @@ class BumpFeeTest(BitcoinTestFramework):
test_nonrbf_bumpfee_fails(self, peer_node, dest_address) test_nonrbf_bumpfee_fails(self, peer_node, dest_address)
test_notmine_bumpfee(self, rbf_node, 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_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_dust_to_fee(self, rbf_node, dest_address)
test_watchonly_psbt(self, peer_node, rbf_node, dest_address) test_watchonly_psbt(self, peer_node, rbf_node, dest_address)
test_rebumping(self, 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() 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): def test_small_output_with_feerate_succeeds(self, rbf_node, dest_address):
self.log.info('Testing small output with feerate bump succeeds') self.log.info('Testing small output with feerate bump succeeds')

View file

@ -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()

View file

@ -447,14 +447,14 @@ class ImportDescriptorsTest(BitcoinTestFramework):
wallet=wmulti_priv) wallet=wmulti_priv)
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1001) # Range end (1000) is inclusive, so 1001 addresses generated 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 assert_equal(addr, 'bcrt1qdt0qy5p7dzhxzmegnn4ulzhard33s2809arjqgjndx87rv5vd0fq2czhy8') # Derived at m/84'/0'/0'/0
change_addr = wmulti_priv.getrawchangeaddress('bech32') change_addr = wmulti_priv.getrawchangeaddress('bech32') # uses change 0
assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e') assert_equal(change_addr, 'bcrt1qt9uhe3a9hnq7vajl7a094z4s3crm9ttf8zw3f5v9gr2nyd7e3lnsy44n8e') # Derived at m/84'/1'/0'/0
assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000) assert_equal(wmulti_priv.getwalletinfo()['keypoolsize'], 1000)
txid = w0.sendtoaddress(addr, 10) txid = w0.sendtoaddress(addr, 10)
self.generate(self.nodes[0], 6) 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'] decoded = wmulti_priv.gettransaction(txid=send_txid, verbose=True)['decoded']
assert_equal(len(decoded['vin'][0]['txinwitness']), 4) assert_equal(len(decoded['vin'][0]['txinwitness']), 4)
self.sync_all() self.sync_all()
@ -480,12 +480,12 @@ class ImportDescriptorsTest(BitcoinTestFramework):
wallet=wmulti_pub) wallet=wmulti_pub)
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 1000) # The first one was already consumed by previous import and is detected as used 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 assert_equal(addr, 'bcrt1qp8s25ckjl7gr6x2q3dx3tn2pytwp05upkjztk6ey857tt50r5aeqn6mvr9') # Derived at m/84'/0'/0'/1
change_addr = wmulti_pub.getrawchangeaddress('bech32') change_addr = wmulti_pub.getrawchangeaddress('bech32') # uses change 2
assert_equal(change_addr, 'bcrt1qzxl0qz2t88kljdnkzg4n4gapr6kte26390gttrg79x66nt4p04fssj53nl') 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 self.nodes[0].getrawmempool(True)
assert(send_txid in (x['txid'] for x in wmulti_pub.listunspent(0))) assert send_txid in (x['txid'] for x in wmulti_pub.listunspent(0))
assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999) assert_equal(wmulti_pub.getwalletinfo()['keypoolsize'], 999)
# generate some utxos for next tests # generate some utxos for next tests

View file

@ -257,7 +257,7 @@ class WalletMigrationTest(BitcoinTestFramework):
imports0 = self.nodes[0].get_wallet_rpc("imports0") imports0 = self.nodes[0].get_wallet_rpc("imports0")
assert_equal(imports0.getwalletinfo()["descriptors"], False) assert_equal(imports0.getwalletinfo()["descriptors"], False)
# Exteranl address label # External address label
imports0.setlabel(default.getnewaddress(), "external") imports0.setlabel(default.getnewaddress(), "external")
# Normal non-watchonly tx # 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_raises_rpc_error(-5, "Invalid or non-wallet transaction id", watchonly.gettransaction, received_txid)
assert_equal(len(watchonly.listtransactions(include_watchonly=True)), 3) 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): def test_no_privkeys(self):
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name) default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
@ -396,11 +403,75 @@ class WalletMigrationTest(BitcoinTestFramework):
def test_encrypted(self): def test_encrypted(self):
self.log.info("Test migration of an encrypted wallet") self.log.info("Test migration of an encrypted wallet")
wallet = self.create_legacy_wallet("encrypted") wallet = self.create_legacy_wallet("encrypted")
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
wallet.encryptwallet("pass") wallet.encryptwallet("pass")
addr = wallet.getnewaddress()
txid = default.sendtoaddress(addr, 1)
self.generate(self.nodes[0], 1)
bals = wallet.getbalances()
assert_raises_rpc_error(-15, "Error: migratewallet on encrypted wallets is currently unsupported.", wallet.migratewallet) assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", wallet.migratewallet)
# TODO: Fix migratewallet so that we can actually migrate encrypted wallets 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(-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): def run_test(self):
self.generate(self.nodes[0], 101) self.generate(self.nodes[0], 101)
@ -412,6 +483,8 @@ class WalletMigrationTest(BitcoinTestFramework):
self.test_no_privkeys() self.test_no_privkeys()
self.test_pk_coinbases() self.test_pk_coinbases()
self.test_encrypted() self.test_encrypted()
self.test_unloaded()
self.test_unloaded_by_path()
if __name__ == '__main__': if __name__ == '__main__':
WalletMigrationTest().main() WalletMigrationTest().main()