Merge bitcoin/bitcoin#27224: refactor: Remove CAddressBookData::destdata

a5986e82dd2b8f923d60f9e31760ef62b9010105 refactor: Remove CAddressBookData::destdata (Ryan Ofsky)
5938ad0bdb013953861c7cd15a95f00998a06f44 wallet: Add DatabaseBatch::ErasePrefix method (Ryan Ofsky)

Pull request description:

  This is cleanup that doesn't change external behavior. Benefits of the cleanup are:

  - Removes awkward `StringMap` intermediate representation for wallet address metadata.
  - Simplifies `CWallet`, deals with used address and received request serialization in `walletdb.cpp` instead of higher level wallet code
  - Adds test coverage and documentation

  This PR doesn't change externally observable behavior. Internally, the only change in behavior is that `EraseDestData` deletes rows directly from the database because they are no longer stored in memory. This is more direct and efficient because it uses a single 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

  ---

  This PR is a rebased copy of #18608. For some reason that PR is locked and couldn't be reopened or commented on.

  This PR is an alternative to #27215 with differences described in https://github.com/bitcoin/bitcoin/pull/27215#pullrequestreview-1329028143

ACKs for top commit:
  achow101:
    ACK a5986e82dd2b8f923d60f9e31760ef62b9010105
  furszy:
    Code ACK a5986e82

Tree-SHA512: 6bd3e402f1f60263fbd433760bdc29d04175ddaf8307207c4a67d59f6cffa732e176ba57886e02926f7a1615dce0ed9cda36c8cbc6430aa8e5b56934c23f3fe7
This commit is contained in:
Andrew Chow 2023-05-01 08:09:36 -04:00
commit 5325a61167
No known key found for this signature in database
GPG Key ID: 17565732E08E5E41
11 changed files with 215 additions and 95 deletions

View File

@ -668,12 +668,14 @@ void BerkeleyDatabase::ReloadDbEnv()
env->ReloadDbEnv(); env->ReloadDbEnv();
} }
BerkeleyCursor::BerkeleyCursor(BerkeleyDatabase& database) BerkeleyCursor::BerkeleyCursor(BerkeleyDatabase& database, BerkeleyBatch* batch)
{ {
if (!database.m_db.get()) { if (!database.m_db.get()) {
throw std::runtime_error(STR_INTERNAL_BUG("BerkeleyDatabase does not exist")); throw std::runtime_error(STR_INTERNAL_BUG("BerkeleyDatabase does not exist"));
} }
int ret = database.m_db->cursor(nullptr, &m_cursor, 0); // Transaction argument to cursor is only needed when using the cursor to
// write to the database. Read-only cursors do not need a txn pointer.
int ret = database.m_db->cursor(batch ? batch->txn() : nullptr, &m_cursor, 0);
if (ret != 0) { if (ret != 0) {
throw std::runtime_error(STR_INTERNAL_BUG(strprintf("BDB Cursor could not be created. Returned %d", ret))); throw std::runtime_error(STR_INTERNAL_BUG(strprintf("BDB Cursor could not be created. Returned %d", ret)));
} }
@ -820,6 +822,25 @@ bool BerkeleyBatch::HasKey(DataStream&& key)
return ret == 0; return ret == 0;
} }
bool BerkeleyBatch::ErasePrefix(Span<const std::byte> prefix)
{
if (!TxnBegin()) return false;
auto cursor{std::make_unique<BerkeleyCursor>(m_database, this)};
// const_cast is safe below even though prefix_key is an in/out parameter,
// because we are not using the DB_DBT_USERMEM flag, so BDB will allocate
// and return a different output data pointer
Dbt prefix_key{const_cast<std::byte*>(prefix.data()), static_cast<uint32_t>(prefix.size())}, prefix_value{};
int ret{cursor->dbc()->get(&prefix_key, &prefix_value, DB_SET_RANGE)};
for (int flag{DB_CURRENT}; ret == 0; flag = DB_NEXT) {
SafeDbt key, value;
ret = cursor->dbc()->get(key, value, flag);
if (ret != 0 || key.get_size() < prefix.size() || memcmp(key.get_data(), prefix.data(), prefix.size()) != 0) break;
ret = cursor->dbc()->del(0);
}
cursor.reset();
return TxnCommit() && (ret == 0 || ret == DB_NOTFOUND);
}
void BerkeleyDatabase::AddRef() void BerkeleyDatabase::AddRef()
{ {
LOCK(cs_db); LOCK(cs_db);

View File

@ -192,10 +192,11 @@ private:
Dbc* m_cursor; Dbc* m_cursor;
public: public:
explicit BerkeleyCursor(BerkeleyDatabase& database); explicit BerkeleyCursor(BerkeleyDatabase& database, BerkeleyBatch* batch=nullptr);
~BerkeleyCursor() override; ~BerkeleyCursor() override;
Status Next(DataStream& key, DataStream& value) override; Status Next(DataStream& key, DataStream& value) override;
Dbc* dbc() const { return m_cursor; }
}; };
/** RAII class that provides access to a Berkeley database */ /** RAII class that provides access to a Berkeley database */
@ -206,6 +207,7 @@ private:
bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override; bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override;
bool EraseKey(DataStream&& key) override; bool EraseKey(DataStream&& key) override;
bool HasKey(DataStream&& key) override; bool HasKey(DataStream&& key) override;
bool ErasePrefix(Span<const std::byte> prefix) override;
protected: protected:
Db* pdb{nullptr}; Db* pdb{nullptr};
@ -230,6 +232,7 @@ public:
bool TxnBegin() override; bool TxnBegin() override;
bool TxnCommit() override; bool TxnCommit() override;
bool TxnAbort() override; bool TxnAbort() override;
DbTxn* txn() const { return activeTxn; }
}; };
std::string BerkeleyDatabaseVersion(); std::string BerkeleyDatabaseVersion();

View File

@ -110,6 +110,7 @@ public:
return HasKey(std::move(ssKey)); return HasKey(std::move(ssKey));
} }
virtual bool ErasePrefix(Span<const std::byte> prefix) = 0;
virtual std::unique_ptr<DatabaseCursor> GetNewCursor() = 0; virtual std::unique_ptr<DatabaseCursor> GetNewCursor() = 0;
virtual bool TxnBegin() = 0; virtual bool TxnBegin() = 0;
@ -186,6 +187,7 @@ private:
bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override { return true; } bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override { return true; }
bool EraseKey(DataStream&& key) override { return true; } bool EraseKey(DataStream&& key) override { return true; }
bool HasKey(DataStream&& key) override { return true; } bool HasKey(DataStream&& key) override { return true; }
bool ErasePrefix(Span<const std::byte> prefix) override { return true; }
public: public:
void Flush() override {} void Flush() override {}

View File

@ -228,9 +228,22 @@ public:
return m_wallet->GetAddressReceiveRequests(); return m_wallet->GetAddressReceiveRequests();
} }
bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) override { bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) override {
// Note: The setAddressReceiveRequest interface used by the GUI to store
// receive requests is a little awkward and could be improved in the
// future:
//
// - The same method is used to save requests and erase them, but
// having separate methods could be clearer and prevent bugs.
//
// - Request ids are passed as strings even though they are generated as
// integers.
//
// - Multiple requests can be stored for the same address, but it might
// be better to only allow one request or only keep the current one.
LOCK(m_wallet->cs_wallet); LOCK(m_wallet->cs_wallet);
WalletBatch batch{m_wallet->GetDatabase()}; WalletBatch batch{m_wallet->GetDatabase()};
return m_wallet->SetAddressReceiveRequest(batch, dest, id, value); return value.empty() ? m_wallet->EraseAddressReceiveRequest(batch, dest, id)
: m_wallet->SetAddressReceiveRequest(batch, dest, id, value);
} }
bool displayAddress(const CTxDestination& dest) override bool displayAddress(const CTxDestination& dest) override
{ {

View File

@ -125,6 +125,7 @@ void SQLiteBatch::SetupSQLStatements()
{&m_insert_stmt, "INSERT INTO main VALUES(?, ?)"}, {&m_insert_stmt, "INSERT INTO main VALUES(?, ?)"},
{&m_overwrite_stmt, "INSERT or REPLACE into main values(?, ?)"}, {&m_overwrite_stmt, "INSERT or REPLACE into main values(?, ?)"},
{&m_delete_stmt, "DELETE FROM main WHERE key = ?"}, {&m_delete_stmt, "DELETE FROM main WHERE key = ?"},
{&m_delete_prefix_stmt, "DELETE FROM main WHERE instr(key, ?) = 1"},
}; };
for (const auto& [stmt_prepared, stmt_text] : statements) { for (const auto& [stmt_prepared, stmt_text] : statements) {
@ -375,6 +376,7 @@ void SQLiteBatch::Close()
{&m_insert_stmt, "insert"}, {&m_insert_stmt, "insert"},
{&m_overwrite_stmt, "overwrite"}, {&m_overwrite_stmt, "overwrite"},
{&m_delete_stmt, "delete"}, {&m_delete_stmt, "delete"},
{&m_delete_prefix_stmt, "delete prefix"},
}; };
for (const auto& [stmt_prepared, stmt_description] : statements) { for (const auto& [stmt_prepared, stmt_description] : statements) {
@ -441,24 +443,34 @@ bool SQLiteBatch::WriteKey(DataStream&& key, DataStream&& value, bool overwrite)
return res == SQLITE_DONE; return res == SQLITE_DONE;
} }
bool SQLiteBatch::EraseKey(DataStream&& key) bool SQLiteBatch::ExecStatement(sqlite3_stmt* stmt, Span<const std::byte> blob)
{ {
if (!m_database.m_db) return false; if (!m_database.m_db) return false;
assert(m_delete_stmt); assert(stmt);
// Bind: leftmost parameter in statement is index 1 // Bind: leftmost parameter in statement is index 1
if (!BindBlobToStatement(m_delete_stmt, 1, key, "key")) return false; if (!BindBlobToStatement(stmt, 1, blob, "key")) return false;
// Execute // Execute
int res = sqlite3_step(m_delete_stmt); int res = sqlite3_step(stmt);
sqlite3_clear_bindings(m_delete_stmt); sqlite3_clear_bindings(stmt);
sqlite3_reset(m_delete_stmt); sqlite3_reset(stmt);
if (res != SQLITE_DONE) { if (res != SQLITE_DONE) {
LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res)); LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res));
} }
return res == SQLITE_DONE; return res == SQLITE_DONE;
} }
bool SQLiteBatch::EraseKey(DataStream&& key)
{
return ExecStatement(m_delete_stmt, key);
}
bool SQLiteBatch::ErasePrefix(Span<const std::byte> prefix)
{
return ExecStatement(m_delete_prefix_stmt, prefix);
}
bool SQLiteBatch::HasKey(DataStream&& key) bool SQLiteBatch::HasKey(DataStream&& key)
{ {
if (!m_database.m_db) return false; if (!m_database.m_db) return false;

View File

@ -36,13 +36,16 @@ private:
sqlite3_stmt* m_insert_stmt{nullptr}; sqlite3_stmt* m_insert_stmt{nullptr};
sqlite3_stmt* m_overwrite_stmt{nullptr}; sqlite3_stmt* m_overwrite_stmt{nullptr};
sqlite3_stmt* m_delete_stmt{nullptr}; sqlite3_stmt* m_delete_stmt{nullptr};
sqlite3_stmt* m_delete_prefix_stmt{nullptr};
void SetupSQLStatements(); void SetupSQLStatements();
bool ExecStatement(sqlite3_stmt* stmt, Span<const std::byte> blob);
bool ReadKey(DataStream&& key, DataStream& value) override; bool ReadKey(DataStream&& key, DataStream& value) override;
bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override; bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override;
bool EraseKey(DataStream&& key) override; bool EraseKey(DataStream&& key) override;
bool HasKey(DataStream&& key) override; bool HasKey(DataStream&& key) override;
bool ErasePrefix(Span<const std::byte> prefix) override;
public: public:
explicit SQLiteBatch(SQLiteDatabase& database); explicit SQLiteBatch(SQLiteDatabase& database);

View File

@ -427,19 +427,63 @@ BOOST_AUTO_TEST_CASE(ComputeTimeSmart)
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300); BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300);
} }
BOOST_AUTO_TEST_CASE(LoadReceiveRequests) static const DatabaseFormat DATABASE_FORMATS[] = {
{ #ifdef USE_SQLITE
CTxDestination dest = PKHash(); DatabaseFormat::SQLITE,
LOCK(m_wallet.cs_wallet); #endif
WalletBatch batch{m_wallet.GetDatabase()}; #ifdef USE_BDB
m_wallet.SetAddressUsed(batch, dest, true); DatabaseFormat::BERKELEY,
m_wallet.SetAddressReceiveRequest(batch, dest, "0", "val_rr0"); #endif
m_wallet.SetAddressReceiveRequest(batch, dest, "1", "val_rr1"); };
auto values = m_wallet.GetAddressReceiveRequests(); void TestLoadWallet(const std::string& name, DatabaseFormat format, std::function<void(std::shared_ptr<CWallet>)> f)
BOOST_CHECK_EQUAL(values.size(), 2U); {
BOOST_CHECK_EQUAL(values[0], "val_rr0"); node::NodeContext node;
BOOST_CHECK_EQUAL(values[1], "val_rr1"); auto chain{interfaces::MakeChain(node)};
DatabaseOptions options;
options.require_format = format;
DatabaseStatus status;
bilingual_str error;
std::vector<bilingual_str> warnings;
auto database{MakeWalletDatabase(name, options, status, error)};
auto wallet{std::make_shared<CWallet>(chain.get(), "", std::move(database))};
BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::LOAD_OK);
WITH_LOCK(wallet->cs_wallet, f(wallet));
}
BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup)
{
for (DatabaseFormat format : DATABASE_FORMATS) {
const std::string name{strprintf("receive-requests-%i", format)};
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
WalletBatch batch{wallet->GetDatabase()};
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), true));
BOOST_CHECK(batch.WriteAddressPreviouslySpent(ScriptHash(), true));
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "0", "val_rr00"));
BOOST_CHECK(wallet->EraseAddressReceiveRequest(batch, PKHash(), "0"));
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr10"));
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr11"));
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, ScriptHash(), "2", "val_rr20"));
});
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
BOOST_CHECK(wallet->IsAddressPreviouslySpent(PKHash()));
BOOST_CHECK(wallet->IsAddressPreviouslySpent(ScriptHash()));
auto requests = wallet->GetAddressReceiveRequests();
auto erequests = {"val_rr11", "val_rr20"};
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
WalletBatch batch{wallet->GetDatabase()};
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
});
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(ScriptHash()));
auto requests = wallet->GetAddressReceiveRequests();
auto erequests = {"val_rr11"};
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
});
}
} }
// Test some watch-only LegacyScriptPubKeyMan methods by the procedure of loading (LoadWatchOnly), // Test some watch-only LegacyScriptPubKeyMan methods by the procedure of loading (LoadWatchOnly),
@ -922,6 +966,7 @@ private:
bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override { return m_pass; } bool WriteKey(DataStream&& key, DataStream&& value, bool overwrite = true) override { return m_pass; }
bool EraseKey(DataStream&& key) override { return m_pass; } bool EraseKey(DataStream&& key) override { return m_pass; }
bool HasKey(DataStream&& key) override { return m_pass; } bool HasKey(DataStream&& key) override { return m_pass; }
bool ErasePrefix(Span<const std::byte> prefix) override { return m_pass; }
public: public:
explicit FailBatch(bool pass) : m_pass(pass) {} explicit FailBatch(bool pass) : m_pass(pass) {}

View File

@ -967,11 +967,11 @@ void CWallet::SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned
CTxDestination dst; CTxDestination dst;
if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) { if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) {
if (IsMine(dst)) { if (IsMine(dst)) {
if (used != IsAddressUsed(dst)) { if (used != IsAddressPreviouslySpent(dst)) {
if (used) { if (used) {
tx_destinations.insert(dst); tx_destinations.insert(dst);
} }
SetAddressUsed(batch, dst, used); SetAddressPreviouslySpent(batch, dst, used);
} }
} }
} }
@ -984,7 +984,7 @@ bool CWallet::IsSpentKey(const CScript& scriptPubKey) const
if (!ExtractDestination(scriptPubKey, dest)) { if (!ExtractDestination(scriptPubKey, dest)) {
return false; return false;
} }
if (IsAddressUsed(dest)) { if (IsAddressPreviouslySpent(dest)) {
return true; return true;
} }
if (IsLegacy()) { if (IsLegacy()) {
@ -992,15 +992,15 @@ bool CWallet::IsSpentKey(const CScript& scriptPubKey) const
assert(spk_man != nullptr); assert(spk_man != nullptr);
for (const auto& keyid : GetAffectedKeys(scriptPubKey, *spk_man)) { for (const auto& keyid : GetAffectedKeys(scriptPubKey, *spk_man)) {
WitnessV0KeyHash wpkh_dest(keyid); WitnessV0KeyHash wpkh_dest(keyid);
if (IsAddressUsed(wpkh_dest)) { if (IsAddressPreviouslySpent(wpkh_dest)) {
return true; return true;
} }
ScriptHash sh_wpkh_dest(GetScriptForDestination(wpkh_dest)); ScriptHash sh_wpkh_dest(GetScriptForDestination(wpkh_dest));
if (IsAddressUsed(sh_wpkh_dest)) { if (IsAddressPreviouslySpent(sh_wpkh_dest)) {
return true; return true;
} }
PKHash pkh_dest(keyid); PKHash pkh_dest(keyid);
if (IsAddressUsed(pkh_dest)) { if (IsAddressPreviouslySpent(pkh_dest)) {
return true; return true;
} }
} }
@ -2403,19 +2403,15 @@ bool CWallet::DelAddressBook(const CTxDestination& address)
WalletBatch batch(GetDatabase()); WalletBatch batch(GetDatabase());
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
// 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) // 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.
// NOTE: This isn't a problem for sending addresses because they never have any DestData yet! // NOTE: This isn't a problem for sending addresses because they don't have any data that needs to be kept.
// When adding new DestData, it should be considered here whether to retain or delete it (or move it?). // When adding new address data, it should be considered here whether to retain or delete it.
if (IsMine(address)) { if (IsMine(address)) {
WalletLogPrintf("%s called with IsMine address, NOT SUPPORTED. Please report this bug! %s\n", __func__, PACKAGE_BUGREPORT); WalletLogPrintf("%s called with IsMine address, NOT SUPPORTED. Please report this bug! %s\n", __func__, PACKAGE_BUGREPORT);
return false; return false;
} }
// Delete destdata tuples associated with address // Delete data rows associated with this address
std::string strAddress = EncodeDestination(address); batch.EraseAddressData(address);
for (const std::pair<const std::string, std::string> &item : m_address_book[address].destdata)
{
batch.EraseDestData(strAddress, item.first);
}
m_address_book.erase(address); m_address_book.erase(address);
} }
@ -2790,51 +2786,42 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx, bool rescanning_old
return nTimeSmart; return nTimeSmart;
} }
bool CWallet::SetAddressUsed(WalletBatch& batch, const CTxDestination& dest, bool used) bool CWallet::SetAddressPreviouslySpent(WalletBatch& batch, const CTxDestination& dest, bool used)
{ {
const std::string key{"used"};
if (std::get_if<CNoDestination>(&dest)) if (std::get_if<CNoDestination>(&dest))
return false; return false;
if (!used) { if (!used) {
if (auto* data = util::FindKey(m_address_book, dest)) data->destdata.erase(key); if (auto* data{util::FindKey(m_address_book, dest)}) data->previously_spent = false;
return batch.EraseDestData(EncodeDestination(dest), key); return batch.WriteAddressPreviouslySpent(dest, false);
} }
const std::string value{"1"}; LoadAddressPreviouslySpent(dest);
m_address_book[dest].destdata.insert(std::make_pair(key, value)); return batch.WriteAddressPreviouslySpent(dest, true);
return batch.WriteDestData(EncodeDestination(dest), key, value);
} }
void CWallet::LoadDestData(const CTxDestination &dest, const std::string &key, const std::string &value) void CWallet::LoadAddressPreviouslySpent(const CTxDestination& dest)
{ {
m_address_book[dest].destdata.insert(std::make_pair(key, value)); m_address_book[dest].previously_spent = true;
} }
bool CWallet::IsAddressUsed(const CTxDestination& dest) const void CWallet::LoadAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& request)
{ {
const std::string key{"used"}; m_address_book[dest].receive_requests[id] = request;
std::map<CTxDestination, CAddressBookData>::const_iterator i = m_address_book.find(dest); }
if(i != m_address_book.end())
{ bool CWallet::IsAddressPreviouslySpent(const CTxDestination& dest) const
CAddressBookData::StringMap::const_iterator j = i->second.destdata.find(key); {
if(j != i->second.destdata.end()) if (auto* data{util::FindKey(m_address_book, dest)}) return data->previously_spent;
{
return true;
}
}
return false; return false;
} }
std::vector<std::string> CWallet::GetAddressReceiveRequests() const std::vector<std::string> CWallet::GetAddressReceiveRequests() const
{ {
const std::string prefix{"rr"};
std::vector<std::string> values; std::vector<std::string> values;
for (const auto& address : m_address_book) { for (const auto& [dest, entry] : m_address_book) {
for (const auto& data : address.second.destdata) { for (const auto& [id, request] : entry.receive_requests) {
if (!data.first.compare(0, prefix.size(), prefix)) { values.emplace_back(request);
values.emplace_back(data.second);
}
} }
} }
return values; return values;
@ -2842,15 +2829,15 @@ std::vector<std::string> CWallet::GetAddressReceiveRequests() const
bool CWallet::SetAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id, const std::string& value) bool CWallet::SetAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id, const std::string& value)
{ {
const std::string key{"rr" + id}; // "rr" prefix = "receive request" in destdata if (!batch.WriteAddressReceiveRequest(dest, id, value)) return false;
CAddressBookData& data = m_address_book.at(dest); m_address_book[dest].receive_requests[id] = value;
if (value.empty()) { return true;
if (!batch.EraseDestData(EncodeDestination(dest), key)) return false; }
data.destdata.erase(key);
} else { bool CWallet::EraseAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id)
if (!batch.WriteDestData(EncodeDestination(dest), key, value)) return false; {
data.destdata[key] = value; if (!batch.EraseAddressReceiveRequest(dest, id)) return false;
} m_address_book[dest].receive_requests.erase(id);
return true; return true;
} }

View File

@ -221,17 +221,22 @@ struct CAddressBookData
std::optional<AddressPurpose> purpose; std::optional<AddressPurpose> purpose;
/** /**
* Additional address metadata map that can currently hold two types of keys: * Whether coins with this address have previously been spent. Set when the
* * the wallet avoid_reuse option is enabled and this is an IsMine address
* "used" keys with values always set to "1" or "p" if present. This is set on * that has already received funds and spent them. This is used during coin
* IsMine addresses that have already been spent from if the * selection to increase privacy by not creating different transactions
* avoid_reuse option is enabled * that spend from the same addresses.
*
* "rr##" keys where ## is a decimal number, with serialized
* RecentRequestEntry objects as values
*/ */
typedef std::map<std::string, std::string> StringMap; bool previously_spent{false};
StringMap destdata;
/**
* Map containing data about previously generated receive requests
* requesting funds to be sent to this address. Only present for IsMine
* addresses. Map keys are decimal numbers uniquely identifying each
* request, and map values are serialized RecentRequestEntry objects
* containing BIP21 URI information including message and amount.
*/
std::map<std::string, std::string> receive_requests{};
/** Accessor methods. */ /** Accessor methods. */
bool IsChange() const { return !label.has_value(); } bool IsChange() const { return !label.has_value(); }
@ -511,8 +516,10 @@ public:
bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; return true; } bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; return true; }
//! Adds a destination data tuple to the store, without saving it to disk //! Marks destination as previously spent.
void LoadDestData(const CTxDestination& dest, const std::string& key, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void LoadAddressPreviouslySpent(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! Appends payment request to destination.
void LoadAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& request) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! Holds a timestamp at which point the wallet is scheduled (externally) to be relocked. Caller must arrange for actual relocking to occur via Lock(). //! Holds a timestamp at which point the wallet is scheduled (externally) to be relocked. Caller must arrange for actual relocking to occur via Lock().
int64_t nRelockTime GUARDED_BY(cs_wallet){0}; int64_t nRelockTime GUARDED_BY(cs_wallet){0};
@ -739,11 +746,12 @@ public:
bool DelAddressBook(const CTxDestination& address); bool DelAddressBook(const CTxDestination& address);
bool IsAddressUsed(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsAddressPreviouslySpent(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool SetAddressUsed(WalletBatch& batch, const CTxDestination& dest, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool SetAddressPreviouslySpent(WalletBatch& batch, const CTxDestination& dest, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
std::vector<std::string> GetAddressReceiveRequests() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::vector<std::string> GetAddressReceiveRequests() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool SetAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool SetAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool EraseAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);

View File

@ -615,7 +615,20 @@ ReadKeyValue(CWallet* pwallet, DataStream& ssKey, CDataStream& ssValue,
ssKey >> strAddress; ssKey >> strAddress;
ssKey >> strKey; ssKey >> strKey;
ssValue >> strValue; ssValue >> strValue;
pwallet->LoadDestData(DecodeDestination(strAddress), strKey, strValue); const CTxDestination& dest{DecodeDestination(strAddress)};
if (strKey.compare("used") == 0) {
// Load "used" key indicating if an IsMine address has
// previously been spent from with avoid_reuse option enabled.
// The strValue is not used for anything currently, but could
// hold more information in the future. Current values are just
// "1" or "p" for present (which was written prior to
// f5ba424cd44619d9b9be88b8593d69a7ba96db26).
pwallet->LoadAddressPreviouslySpent(dest);
} else if (strKey.compare(0, 2, "rr") == 0) {
// Load "rr##" keys where ## is a decimal number, and strValue
// is a serialized RecentRequestEntry object.
pwallet->LoadAddressReceiveRequest(dest, strKey.substr(2), strValue);
}
} else if (strType == DBKeys::HDCHAIN) { } else if (strType == DBKeys::HDCHAIN) {
CHDChain chain; CHDChain chain;
ssValue >> chain; ssValue >> chain;
@ -1088,16 +1101,28 @@ void MaybeCompactWalletDB(WalletContext& context)
fOneThread = false; fOneThread = false;
} }
bool WalletBatch::WriteDestData(const std::string &address, const std::string &key, const std::string &value) bool WalletBatch::WriteAddressPreviouslySpent(const CTxDestination& dest, bool previously_spent)
{ {
return WriteIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(address, key)), value); auto key{std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), std::string("used")))};
return previously_spent ? WriteIC(key, std::string("1")) : EraseIC(key);
} }
bool WalletBatch::EraseDestData(const std::string &address, const std::string &key) bool WalletBatch::WriteAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& receive_request)
{ {
return EraseIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(address, key))); return WriteIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), "rr" + id)), receive_request);
} }
bool WalletBatch::EraseAddressReceiveRequest(const CTxDestination& dest, const std::string& id)
{
return EraseIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), "rr" + id)));
}
bool WalletBatch::EraseAddressData(const CTxDestination& dest)
{
DataStream prefix;
prefix << DBKeys::DESTDATA << EncodeDestination(dest);
return m_batch->ErasePrefix(prefix);
}
bool WalletBatch::WriteHDChain(const CHDChain& chain) bool WalletBatch::WriteHDChain(const CHDChain& chain)
{ {

View File

@ -7,6 +7,7 @@
#define BITCOIN_WALLET_WALLETDB_H #define BITCOIN_WALLET_WALLETDB_H
#include <script/sign.h> #include <script/sign.h>
#include <script/standard.h>
#include <wallet/db.h> #include <wallet/db.h>
#include <wallet/walletutil.h> #include <wallet/walletutil.h>
#include <key.h> #include <key.h>
@ -264,10 +265,10 @@ public:
bool WriteLockedUTXO(const COutPoint& output); bool WriteLockedUTXO(const COutPoint& output);
bool EraseLockedUTXO(const COutPoint& output); bool EraseLockedUTXO(const COutPoint& output);
/// Write destination data key,value tuple to database bool WriteAddressPreviouslySpent(const CTxDestination& dest, bool previously_spent);
bool WriteDestData(const std::string &address, const std::string &key, const std::string &value); bool WriteAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& receive_request);
/// Erase destination data tuple from wallet database bool EraseAddressReceiveRequest(const CTxDestination& dest, const std::string& id);
bool EraseDestData(const std::string &address, const std::string &key); bool EraseAddressData(const CTxDestination& dest);
bool WriteActiveScriptPubKeyMan(uint8_t type, const uint256& id, bool internal); bool WriteActiveScriptPubKeyMan(uint8_t type, const uint256& id, bool internal);
bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal); bool EraseActiveScriptPubKeyMan(uint8_t type, bool internal);