From b58acfcd117df24c42bc300192ad22aa1652e42e Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Mon, 18 Dec 2023 02:13:47 +0000 Subject: [PATCH 1/3] Wallet: Keep track of what addresses are used in wallet transactions (memory only) Github-Pull: #22693 Rebased-From: fc7954a1488b815dac4311b5a461fe5bf2a20b19 --- src/wallet/wallet.cpp | 28 ++++++++++++++++++++++++++-- src/wallet/wallet.h | 12 +++++++++++- src/wallet/wallettool.cpp | 7 ++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 78febb8195..fcae1f5b2f 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -786,6 +786,23 @@ void CWallet::AddToSpends(const CWalletTx& wtx, WalletBatch* batch) AddToSpends(txin.prevout, wtx.GetHash(), batch); } +void CWallet::InitialiseAddressBookUsed() +{ + for (const auto& entry : mapWallet) { + const CWalletTx& wtx = entry.second; + UpdateAddressBookUsed(wtx); + } +} + +void CWallet::UpdateAddressBookUsed(const CWalletTx& wtx) +{ + for (const auto& output : wtx.tx->vout) { + CTxDestination dest; + if (!ExtractDestination(output.scriptPubKey, dest)) continue; + m_address_book[dest].m_used = true; + } +} + bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) { if (IsCrypted()) @@ -1083,6 +1100,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const TxState& state, const // Update birth time when tx time is older than it. MaybeUpdateBirthTime(wtx.GetTxTime()); + UpdateAddressBookUsed(wtx); } if (!fInsertedNew) @@ -2311,7 +2329,7 @@ void CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve } } -DBErrors CWallet::LoadWallet() +DBErrors CWallet::LoadWallet(const do_init_used_flag do_init_used_flag_val) { LOCK(cs_wallet); @@ -2331,7 +2349,13 @@ DBErrors CWallet::LoadWallet() assert(m_internal_spk_managers.empty()); } - return nLoadWalletRet; + if (nLoadWalletRet != DBErrors::LOAD_OK) { + return nLoadWalletRet; + } + + if (do_init_used_flag_val == do_init_used_flag::Init) InitialiseAddressBookUsed(); + + return DBErrors::LOAD_OK; } DBErrors CWallet::ZapSelectTx(std::vector& vHashIn, std::vector& vHashOut) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index c7cd3b3959..67c4a20c9b 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -237,6 +237,12 @@ struct CAddressBookData */ std::optional label; + /** Whether address is the destination of any wallet transation. + * Unlike other fields in address data struct, the used value is determined + * at runtime and not serialized as part of address data. + */ + bool m_used{false}; + /** * Address purpose which was originally recorded for payment protocol * support but now serves as a cached IsMine value. Wallet code should @@ -337,6 +343,9 @@ private: void AddToSpends(const COutPoint& outpoint, const uint256& wtxid, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void AddToSpends(const CWalletTx& wtx, WalletBatch* batch = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void InitialiseAddressBookUsed() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void UpdateAddressBookUsed(const CWalletTx&) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /** * Add a transaction to the wallet, or update it. confirm.block_* should * be set when the transaction was known to be included in a block. When @@ -779,7 +788,8 @@ public: CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const; void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override; - DBErrors LoadWallet(); + enum class do_init_used_flag { Init, Skip }; + DBErrors LoadWallet(const do_init_used_flag do_init_used_flag_val = do_init_used_flag::Init); DBErrors ZapSelectTx(std::vector& vHashIn, std::vector& vHashOut) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool SetAddressBook(const CTxDestination& address, const std::string& strName, const std::optional& purpose); diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index 2f3f8ef77d..39a0113042 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -47,7 +47,7 @@ static void WalletCreate(CWallet* wallet_instance, uint64_t wallet_creation_flag wallet_instance->TopUpKeyPool(); } -static std::shared_ptr MakeWallet(const std::string& name, const fs::path& path, DatabaseOptions options) +static std::shared_ptr MakeWallet(const std::string& name, const fs::path& path, DatabaseOptions options, CWallet::do_init_used_flag do_init_used_flag_val = CWallet::do_init_used_flag::Init) { DatabaseStatus status; bilingual_str error; @@ -61,7 +61,7 @@ static std::shared_ptr MakeWallet(const std::string& name, const fs::pa std::shared_ptr wallet_instance{new CWallet(/*chain=*/nullptr, name, std::move(database)), WalletToolReleaseWallet}; DBErrors load_wallet_ret; try { - load_wallet_ret = wallet_instance->LoadWallet(); + load_wallet_ret = wallet_instance->LoadWallet(do_init_used_flag_val); } catch (const std::runtime_error&) { tfm::format(std::cerr, "Error loading %s. Is wallet being used by another process?\n", name); return nullptr; @@ -168,7 +168,8 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) DatabaseOptions options; ReadDatabaseArgs(args, options); options.require_existing = true; - const std::shared_ptr wallet_instance = MakeWallet(name, path, options); + // NOTE: We need to skip initialisation of the m_used flag, or else the address book count might be wrong + const std::shared_ptr wallet_instance = MakeWallet(name, path, options, CWallet::do_init_used_flag::Skip); if (!wallet_instance) return false; WalletShowInfo(wallet_instance.get()); wallet_instance->Close(); From 559a93d16742553c27c7dfff655ab7a0dad4ac3a Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Thu, 12 Aug 2021 20:52:10 +0000 Subject: [PATCH 2/3] Wallet: Add fairly-efficient [negative] check that an address is not known to be used Github-Pull: #22693 Rebased-From: 022887d933db47227db5dddb75cc6377b4c1b889 --- src/wallet/wallet.cpp | 37 +++++++++++++++++++++++++++++++++++++ src/wallet/wallet.h | 3 +++ 2 files changed, 40 insertions(+) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index fcae1f5b2f..8670a56ac6 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -803,6 +803,43 @@ void CWallet::UpdateAddressBookUsed(const CWalletTx& wtx) } } +bool CWallet::FindScriptPubKeyUsed(const std::set& keys, const std::variant, std::function>& callback) const +{ + AssertLockHeld(cs_wallet); + bool found_any = false; + for (const auto& key : keys) { + CTxDestination dest; + if (!ExtractDestination(key, dest)) continue; + const auto& address_book_it = m_address_book.find(dest); + if (address_book_it == m_address_book.end()) continue; + if (address_book_it->second.m_used) { + found_any = true; + break; + } + } + if (!found_any) return false; + if (std::holds_alternative(callback)) return true; + + found_any = false; + for (const auto& entry : mapWallet) { + const CWalletTx& wtx = entry.second; + for (size_t i = 0; i < wtx.tx->vout.size(); ++i) { + const auto& output = wtx.tx->vout[i]; + if (keys.count(output.scriptPubKey)) { + found_any = true; + const auto callback_type = callback.index(); + if (callback_type == 1) { + std::get>(callback)(wtx); + break; + } + std::get>(callback)(wtx, i); + } + } + } + + return found_any; +} + bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) { if (IsCrypted()) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 67c4a20c9b..0a9b868ba2 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -546,6 +547,8 @@ public: bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void ListLockedCoins(std::vector& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool FindScriptPubKeyUsed(const std::set& keys, const std::variant, std::function>& callback = std::monostate()) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + /* * Rescan abort properties */ From 06aafaaa85e6e0e2026acfda74441d3c6b318ec1 Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Sun, 5 May 2019 04:10:54 +0000 Subject: [PATCH 3/3] RPC/Wallet: Add "use_txids" to output of getaddressinfo Github-Pull: #22693 Rebased-From: a00bc6f395ecd2f9657b9edd4c9c77883a0dc718 --- src/wallet/rpc/addresses.cpp | 13 +++++++++++++ test/functional/wallet_basic.py | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/wallet/rpc/addresses.cpp b/src/wallet/rpc/addresses.cpp index e9b93afc30..9a99156cf8 100644 --- a/src/wallet/rpc/addresses.cpp +++ b/src/wallet/rpc/addresses.cpp @@ -538,6 +538,10 @@ RPCHelpMan getaddressinfo() { {RPCResult::Type::STR, "label name", "Label name (defaults to \"\")."}, }}, + {RPCResult::Type::ARR, "use_txids", "", + { + {RPCResult::Type::STR_HEX, "txid", "The ids of transactions involving this wallet which received with the address"}, + }}, } }, RPCExamples{ @@ -631,6 +635,15 @@ RPCHelpMan getaddressinfo() } ret.pushKV("labels", std::move(labels)); + // NOTE: Intentionally not special-casing a single txid: while addresses + // should never be reused, it's not unexpected to have RBF result in + // multiple txids for a single use. + UniValue use_txids(UniValue::VARR); + pwallet->FindScriptPubKeyUsed(std::set{scriptPubKey}, [&use_txids](const CWalletTx&wtx) { + use_txids.push_back(wtx.GetHash().GetHex()); + }); + ret.pushKV("use_txids", std::move(use_txids)); + return ret; }, }; diff --git a/test/functional/wallet_basic.py b/test/functional/wallet_basic.py index 01149a0977..0f48ee5ab7 100755 --- a/test/functional/wallet_basic.py +++ b/test/functional/wallet_basic.py @@ -655,6 +655,16 @@ class WalletTest(BitcoinTestFramework): assert not address_info["iswatchonly"] assert not address_info["isscript"] assert not address_info["ischange"] + assert_equal(address_info['use_txids'], []) + + # Test getaddressinfo 'use_txids' field + addr = "mneYUmWYsuk7kySiURxCi3AGxrAqZxLgPZ" + txid_1 = self.nodes[0].sendtoaddress(addr, 1) + address_info = self.nodes[0].getaddressinfo(addr) + assert_equal(address_info['use_txids'], [txid_1]) + txid_2 = self.nodes[0].sendtoaddress(addr, 1) + address_info = self.nodes[0].getaddressinfo(addr) + assert_equal(sorted(address_info['use_txids']), sorted([txid_1, txid_2])) # Test getaddressinfo 'ischange' field on change address. self.generate(self.nodes[0], 1, sync_fun=self.no_op)