Skip to content

Commit a5986e8

Browse files
ryanofskyfurszy
andcommitted
refactor: Remove CAddressBookData::destdata
This is cleanup that doesn't change external behavior. - Removes awkward `StringMap` intermediate representation - Simplifies CWallet code, deals with used address and received request serialization in walletdb.cpp - Adds test coverage and documentation - Reduces memory usage This PR doesn't change externally observable behavior. Internally, only change in behavior is that EraseDestData deletes directly from database because the `StringMap` is gone. This is more direct and efficient because it uses a single btree lookup and scan instead of multiple lookups Motivation for this cleanup is making changes like #18550, #18192, #13756 easier to reason about and less likely to result in unintended behavior and bugs Co-authored-by: furszy <matiasfurszyfer@protonmail.com>
1 parent 5938ad0 commit a5986e8

File tree

6 files changed

+164
-86
lines changed

6 files changed

+164
-86
lines changed

src/wallet/interfaces.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,22 @@ class WalletImpl : public Wallet
228228
return m_wallet->GetAddressReceiveRequests();
229229
}
230230
bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) override {
231+
// Note: The setAddressReceiveRequest interface used by the GUI to store
232+
// receive requests is a little awkward and could be improved in the
233+
// future:
234+
//
235+
// - The same method is used to save requests and erase them, but
236+
// having separate methods could be clearer and prevent bugs.
237+
//
238+
// - Request ids are passed as strings even though they are generated as
239+
// integers.
240+
//
241+
// - Multiple requests can be stored for the same address, but it might
242+
// be better to only allow one request or only keep the current one.
231243
LOCK(m_wallet->cs_wallet);
232244
WalletBatch batch{m_wallet->GetDatabase()};
233-
return m_wallet->SetAddressReceiveRequest(batch, dest, id, value);
245+
return value.empty() ? m_wallet->EraseAddressReceiveRequest(batch, dest, id)
246+
: m_wallet->SetAddressReceiveRequest(batch, dest, id, value);
234247
}
235248
bool displayAddress(const CTxDestination& dest) override
236249
{

src/wallet/test/wallet_tests.cpp

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -427,19 +427,63 @@ BOOST_AUTO_TEST_CASE(ComputeTimeSmart)
427427
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300);
428428
}
429429

430-
BOOST_AUTO_TEST_CASE(LoadReceiveRequests)
430+
static const DatabaseFormat DATABASE_FORMATS[] = {
431+
#ifdef USE_SQLITE
432+
DatabaseFormat::SQLITE,
433+
#endif
434+
#ifdef USE_BDB
435+
DatabaseFormat::BERKELEY,
436+
#endif
437+
};
438+
439+
void TestLoadWallet(const std::string& name, DatabaseFormat format, std::function<void(std::shared_ptr<CWallet>)> f)
431440
{
432-
CTxDestination dest = PKHash();
433-
LOCK(m_wallet.cs_wallet);
434-
WalletBatch batch{m_wallet.GetDatabase()};
435-
m_wallet.SetAddressUsed(batch, dest, true);
436-
m_wallet.SetAddressReceiveRequest(batch, dest, "0", "val_rr0");
437-
m_wallet.SetAddressReceiveRequest(batch, dest, "1", "val_rr1");
438-
439-
auto values = m_wallet.GetAddressReceiveRequests();
440-
BOOST_CHECK_EQUAL(values.size(), 2U);
441-
BOOST_CHECK_EQUAL(values[0], "val_rr0");
442-
BOOST_CHECK_EQUAL(values[1], "val_rr1");
441+
node::NodeContext node;
442+
auto chain{interfaces::MakeChain(node)};
443+
DatabaseOptions options;
444+
options.require_format = format;
445+
DatabaseStatus status;
446+
bilingual_str error;
447+
std::vector<bilingual_str> warnings;
448+
auto database{MakeWalletDatabase(name, options, status, error)};
449+
auto wallet{std::make_shared<CWallet>(chain.get(), "", std::move(database))};
450+
BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::LOAD_OK);
451+
WITH_LOCK(wallet->cs_wallet, f(wallet));
452+
}
453+
454+
BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup)
455+
{
456+
for (DatabaseFormat format : DATABASE_FORMATS) {
457+
const std::string name{strprintf("receive-requests-%i", format)};
458+
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
459+
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
460+
WalletBatch batch{wallet->GetDatabase()};
461+
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), true));
462+
BOOST_CHECK(batch.WriteAddressPreviouslySpent(ScriptHash(), true));
463+
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "0", "val_rr00"));
464+
BOOST_CHECK(wallet->EraseAddressReceiveRequest(batch, PKHash(), "0"));
465+
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr10"));
466+
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr11"));
467+
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, ScriptHash(), "2", "val_rr20"));
468+
});
469+
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
470+
BOOST_CHECK(wallet->IsAddressPreviouslySpent(PKHash()));
471+
BOOST_CHECK(wallet->IsAddressPreviouslySpent(ScriptHash()));
472+
auto requests = wallet->GetAddressReceiveRequests();
473+
auto erequests = {"val_rr11", "val_rr20"};
474+
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
475+
WalletBatch batch{wallet->GetDatabase()};
476+
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
477+
BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
478+
});
479+
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
480+
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
481+
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(ScriptHash()));
482+
auto requests = wallet->GetAddressReceiveRequests();
483+
auto erequests = {"val_rr11"};
484+
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
485+
});
486+
}
443487
}
444488

445489
// Test some watch-only LegacyScriptPubKeyMan methods by the procedure of loading (LoadWatchOnly),

src/wallet/wallet.cpp

Lines changed: 37 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -974,11 +974,11 @@ void CWallet::SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned
974974
CTxDestination dst;
975975
if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) {
976976
if (IsMine(dst)) {
977-
if (used != IsAddressUsed(dst)) {
977+
if (used != IsAddressPreviouslySpent(dst)) {
978978
if (used) {
979979
tx_destinations.insert(dst);
980980
}
981-
SetAddressUsed(batch, dst, used);
981+
SetAddressPreviouslySpent(batch, dst, used);
982982
}
983983
}
984984
}
@@ -991,23 +991,23 @@ bool CWallet::IsSpentKey(const CScript& scriptPubKey) const
991991
if (!ExtractDestination(scriptPubKey, dest)) {
992992
return false;
993993
}
994-
if (IsAddressUsed(dest)) {
994+
if (IsAddressPreviouslySpent(dest)) {
995995
return true;
996996
}
997997
if (IsLegacy()) {
998998
LegacyScriptPubKeyMan* spk_man = GetLegacyScriptPubKeyMan();
999999
assert(spk_man != nullptr);
10001000
for (const auto& keyid : GetAffectedKeys(scriptPubKey, *spk_man)) {
10011001
WitnessV0KeyHash wpkh_dest(keyid);
1002-
if (IsAddressUsed(wpkh_dest)) {
1002+
if (IsAddressPreviouslySpent(wpkh_dest)) {
10031003
return true;
10041004
}
10051005
ScriptHash sh_wpkh_dest(GetScriptForDestination(wpkh_dest));
1006-
if (IsAddressUsed(sh_wpkh_dest)) {
1006+
if (IsAddressPreviouslySpent(sh_wpkh_dest)) {
10071007
return true;
10081008
}
10091009
PKHash pkh_dest(keyid);
1010-
if (IsAddressUsed(pkh_dest)) {
1010+
if (IsAddressPreviouslySpent(pkh_dest)) {
10111011
return true;
10121012
}
10131013
}
@@ -2437,19 +2437,15 @@ bool CWallet::DelAddressBook(const CTxDestination& address)
24372437
WalletBatch batch(GetDatabase());
24382438
{
24392439
LOCK(cs_wallet);
2440-
// If we want to delete receiving addresses, we need to take care that DestData "used" (and possibly newer DestData) gets preserved (and the "deleted" address transformed into a change entry instead of actually being deleted)
2441-
// NOTE: This isn't a problem for sending addresses because they never have any DestData yet!
2442-
// When adding new DestData, it should be considered here whether to retain or delete it (or move it?).
2440+
// If we want to delete receiving addresses, we should avoid calling EraseAddressData because it will delete the previously_spent value. Could instead just erase the label so it becomes a change address, and keep the data.
2441+
// NOTE: This isn't a problem for sending addresses because they don't have any data that needs to be kept.
2442+
// When adding new address data, it should be considered here whether to retain or delete it.
24432443
if (IsMine(address)) {
24442444
WalletLogPrintf("%s called with IsMine address, NOT SUPPORTED. Please report this bug! %s\n", __func__, PACKAGE_BUGREPORT);
24452445
return false;
24462446
}
2447-
// Delete destdata tuples associated with address
2448-
std::string strAddress = EncodeDestination(address);
2449-
for (const std::pair<const std::string, std::string> &item : m_address_book[address].destdata)
2450-
{
2451-
batch.EraseDestData(strAddress, item.first);
2452-
}
2447+
// Delete data rows associated with this address
2448+
batch.EraseAddressData(address);
24532449
m_address_book.erase(address);
24542450
}
24552451

@@ -2824,67 +2820,58 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx, bool rescanning_old
28242820
return nTimeSmart;
28252821
}
28262822

2827-
bool CWallet::SetAddressUsed(WalletBatch& batch, const CTxDestination& dest, bool used)
2823+
bool CWallet::SetAddressPreviouslySpent(WalletBatch& batch, const CTxDestination& dest, bool used)
28282824
{
2829-
const std::string key{"used"};
28302825
if (std::get_if<CNoDestination>(&dest))
28312826
return false;
28322827

28332828
if (!used) {
2834-
if (auto* data = util::FindKey(m_address_book, dest)) data->destdata.erase(key);
2835-
return batch.EraseDestData(EncodeDestination(dest), key);
2829+
if (auto* data{util::FindKey(m_address_book, dest)}) data->previously_spent = false;
2830+
return batch.WriteAddressPreviouslySpent(dest, false);
28362831
}
28372832

2838-
const std::string value{"1"};
2839-
m_address_book[dest].destdata.insert(std::make_pair(key, value));
2840-
return batch.WriteDestData(EncodeDestination(dest), key, value);
2833+
LoadAddressPreviouslySpent(dest);
2834+
return batch.WriteAddressPreviouslySpent(dest, true);
28412835
}
28422836

2843-
void CWallet::LoadDestData(const CTxDestination &dest, const std::string &key, const std::string &value)
2837+
void CWallet::LoadAddressPreviouslySpent(const CTxDestination& dest)
28442838
{
2845-
m_address_book[dest].destdata.insert(std::make_pair(key, value));
2839+
m_address_book[dest].previously_spent = true;
28462840
}
28472841

2848-
bool CWallet::IsAddressUsed(const CTxDestination& dest) const
2842+
void CWallet::LoadAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& request)
28492843
{
2850-
const std::string key{"used"};
2851-
std::map<CTxDestination, CAddressBookData>::const_iterator i = m_address_book.find(dest);
2852-
if(i != m_address_book.end())
2853-
{
2854-
CAddressBookData::StringMap::const_iterator j = i->second.destdata.find(key);
2855-
if(j != i->second.destdata.end())
2856-
{
2857-
return true;
2858-
}
2859-
}
2844+
m_address_book[dest].receive_requests[id] = request;
2845+
}
2846+
2847+
bool CWallet::IsAddressPreviouslySpent(const CTxDestination& dest) const
2848+
{
2849+
if (auto* data{util::FindKey(m_address_book, dest)}) return data->previously_spent;
28602850
return false;
28612851
}
28622852

28632853
std::vector<std::string> CWallet::GetAddressReceiveRequests() const
28642854
{
2865-
const std::string prefix{"rr"};
28662855
std::vector<std::string> values;
2867-
for (const auto& address : m_address_book) {
2868-
for (const auto& data : address.second.destdata) {
2869-
if (!data.first.compare(0, prefix.size(), prefix)) {
2870-
values.emplace_back(data.second);
2871-
}
2856+
for (const auto& [dest, entry] : m_address_book) {
2857+
for (const auto& [id, request] : entry.receive_requests) {
2858+
values.emplace_back(request);
28722859
}
28732860
}
28742861
return values;
28752862
}
28762863

28772864
bool CWallet::SetAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id, const std::string& value)
28782865
{
2879-
const std::string key{"rr" + id}; // "rr" prefix = "receive request" in destdata
2880-
CAddressBookData& data = m_address_book.at(dest);
2881-
if (value.empty()) {
2882-
if (!batch.EraseDestData(EncodeDestination(dest), key)) return false;
2883-
data.destdata.erase(key);
2884-
} else {
2885-
if (!batch.WriteDestData(EncodeDestination(dest), key, value)) return false;
2886-
data.destdata[key] = value;
2887-
}
2866+
if (!batch.WriteAddressReceiveRequest(dest, id, value)) return false;
2867+
m_address_book[dest].receive_requests[id] = value;
2868+
return true;
2869+
}
2870+
2871+
bool CWallet::EraseAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id)
2872+
{
2873+
if (!batch.EraseAddressReceiveRequest(dest, id)) return false;
2874+
m_address_book[dest].receive_requests.erase(id);
28882875
return true;
28892876
}
28902877

src/wallet/wallet.h

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -223,17 +223,22 @@ struct CAddressBookData
223223
std::optional<AddressPurpose> purpose;
224224

225225
/**
226-
* Additional address metadata map that can currently hold two types of keys:
227-
*
228-
* "used" keys with values always set to "1" or "p" if present. This is set on
229-
* IsMine addresses that have already been spent from if the
230-
* avoid_reuse option is enabled
231-
*
232-
* "rr##" keys where ## is a decimal number, with serialized
233-
* RecentRequestEntry objects as values
226+
* Whether coins with this address have previously been spent. Set when the
227+
* the wallet avoid_reuse option is enabled and this is an IsMine address
228+
* that has already received funds and spent them. This is used during coin
229+
* selection to increase privacy by not creating different transactions
230+
* that spend from the same addresses.
231+
*/
232+
bool previously_spent{false};
233+
234+
/**
235+
* Map containing data about previously generated receive requests
236+
* requesting funds to be sent to this address. Only present for IsMine
237+
* addresses. Map keys are decimal numbers uniquely identifying each
238+
* request, and map values are serialized RecentRequestEntry objects
239+
* containing BIP21 URI information including message and amount.
234240
*/
235-
typedef std::map<std::string, std::string> StringMap;
236-
StringMap destdata;
241+
std::map<std::string, std::string> receive_requests{};
237242

238243
/** Accessor methods. */
239244
bool IsChange() const { return !label.has_value(); }
@@ -513,8 +518,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
513518

514519
bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; return true; }
515520

516-
//! Adds a destination data tuple to the store, without saving it to disk
517-
void LoadDestData(const CTxDestination& dest, const std::string& key, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
521+
//! Marks destination as previously spent.
522+
void LoadAddressPreviouslySpent(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
523+
//! Appends payment request to destination.
524+
void LoadAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& request) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
518525

519526
//! Holds a timestamp at which point the wallet is scheduled (externally) to be relocked. Caller must arrange for actual relocking to occur via Lock().
520527
int64_t nRelockTime GUARDED_BY(cs_wallet){0};
@@ -741,11 +748,12 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
741748

742749
bool DelAddressBook(const CTxDestination& address);
743750

744-
bool IsAddressUsed(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
745-
bool SetAddressUsed(WalletBatch& batch, const CTxDestination& dest, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
751+
bool IsAddressPreviouslySpent(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
752+
bool SetAddressPreviouslySpent(WalletBatch& batch, const CTxDestination& dest, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
746753

747754
std::vector<std::string> GetAddressReceiveRequests() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
748755
bool SetAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
756+
bool EraseAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
749757

750758
unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
751759

src/wallet/walletdb.cpp

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,20 @@ ReadKeyValue(CWallet* pwallet, DataStream& ssKey, CDataStream& ssValue,
615615
ssKey >> strAddress;
616616
ssKey >> strKey;
617617
ssValue >> strValue;
618-
pwallet->LoadDestData(DecodeDestination(strAddress), strKey, strValue);
618+
const CTxDestination& dest{DecodeDestination(strAddress)};
619+
if (strKey.compare("used") == 0) {
620+
// Load "used" key indicating if an IsMine address has
621+
// previously been spent from with avoid_reuse option enabled.
622+
// The strValue is not used for anything currently, but could
623+
// hold more information in the future. Current values are just
624+
// "1" or "p" for present (which was written prior to
625+
// f5ba424cd44619d9b9be88b8593d69a7ba96db26).
626+
pwallet->LoadAddressPreviouslySpent(dest);
627+
} else if (strKey.compare(0, 2, "rr") == 0) {
628+
// Load "rr##" keys where ## is a decimal number, and strValue
629+
// is a serialized RecentRequestEntry object.
630+
pwallet->LoadAddressReceiveRequest(dest, strKey.substr(2), strValue);
631+
}
619632
} else if (strType == DBKeys::HDCHAIN) {
620633
CHDChain chain;
621634
ssValue >> chain;
@@ -1088,16 +1101,28 @@ void MaybeCompactWalletDB(WalletContext& context)
10881101
fOneThread = false;
10891102
}
10901103

1091-
bool WalletBatch::WriteDestData(const std::string &address, const std::string &key, const std::string &value)
1104+
bool WalletBatch::WriteAddressPreviouslySpent(const CTxDestination& dest, bool previously_spent)
1105+
{
1106+
auto key{std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), std::string("used")))};
1107+
return previously_spent ? WriteIC(key, std::string("1")) : EraseIC(key);
1108+
}
1109+
1110+
bool WalletBatch::WriteAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& receive_request)
10921111
{
1093-
return WriteIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(address, key)), value);
1112+
return WriteIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), "rr" + id)), receive_request);
10941113
}
10951114

1096-
bool WalletBatch::EraseDestData(const std::string &address, const std::string &key)
1115+
bool WalletBatch::EraseAddressReceiveRequest(const CTxDestination& dest, const std::string& id)
10971116
{
1098-
return EraseIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(address, key)));
1117+
return EraseIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), "rr" + id)));
10991118
}
11001119

1120+
bool WalletBatch::EraseAddressData(const CTxDestination& dest)
1121+
{
1122+
DataStream prefix;
1123+
prefix << DBKeys::DESTDATA << EncodeDestination(dest);
1124+
return m_batch->ErasePrefix(prefix);
1125+
}
11011126

11021127
bool WalletBatch::WriteHDChain(const CHDChain& chain)
11031128
{

src/wallet/walletdb.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#define BITCOIN_WALLET_WALLETDB_H
88

99
#include <script/sign.h>
10+
#include <script/standard.h>
1011
#include <wallet/db.h>
1112
#include <wallet/walletutil.h>
1213
#include <key.h>
@@ -264,10 +265,10 @@ class WalletBatch
264265
bool WriteLockedUTXO(const COutPoint& output);
265266
bool EraseLockedUTXO(const COutPoint& output);
266267

267-
/// Write destination data key,value tuple to database
268-
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value);
269-
/// Erase destination data tuple from wallet database
270-
bool EraseDestData(const std::string &address, const std::string &key);
268+
bool WriteAddressPreviouslySpent(const CTxDestination& dest, bool previously_spent);
269+
bool WriteAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& receive_request);
270+
bool EraseAddressReceiveRequest(const CTxDestination& dest, const std::string& id);
271+
bool EraseAddressData(const CTxDestination& dest);
271272

272273
bool WriteActiveScriptPubKeyMan(uint8_t type, const uint256& id, bool internal);
273274
bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal);

0 commit comments

Comments
 (0)