diff --git a/src/wallet/bdb.cpp b/src/wallet/bdb.cpp index 65fb838f655c..b42b8e171385 100644 --- a/src/wallet/bdb.cpp +++ b/src/wallet/bdb.cpp @@ -816,6 +816,29 @@ bool BerkeleyBatch::HasKey(CDataStream&& key) return ret == 0; } +bool BerkeleyBatch::ErasePrefix(Span prefix) +{ + if (!TxnBegin()) return false; + if (!StartCursor()) { + TxnAbort(); + return false; + } + + // 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(prefix.data()), static_cast(prefix.size())}, prefix_value{}; + int ret{m_cursor->get(&prefix_key, &prefix_value, DB_SET_RANGE)}; + for (int flag{DB_CURRENT}; ret == 0; flag = DB_NEXT) { + SafeDbt key, value; + ret = m_cursor->get(key, value, flag); + if (ret != 0 || key.get_size() < prefix.size() || memcmp(key.get_data(), prefix.data(), prefix.size()) != 0) break; + ret = m_cursor->del(0); + } + CloseCursor(); + return TxnCommit() && (ret == 0 || ret == DB_NOTFOUND); +} + void BerkeleyDatabase::AddRef() { LOCK(cs_db); diff --git a/src/wallet/bdb.h b/src/wallet/bdb.h index 8a4feb696bc8..21c6676ac17a 100644 --- a/src/wallet/bdb.h +++ b/src/wallet/bdb.h @@ -194,6 +194,7 @@ class BerkeleyBatch : public DatabaseBatch bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite = true) override; bool EraseKey(CDataStream&& key) override; bool HasKey(CDataStream&& key) override; + bool ErasePrefix(Span prefix) override; protected: Db* pdb; @@ -221,6 +222,7 @@ class BerkeleyBatch : public DatabaseBatch bool TxnBegin() override; bool TxnCommit() override; bool TxnAbort() override; + DbTxn* txn() const { return activeTxn; } }; std::string BerkeleyDatabaseVersion(); diff --git a/src/wallet/db.h b/src/wallet/db.h index dba86c8a90bd..263c0182fc0c 100644 --- a/src/wallet/db.h +++ b/src/wallet/db.h @@ -91,6 +91,7 @@ class DatabaseBatch return HasKey(std::move(ssKey)); } + virtual bool ErasePrefix(Span prefix) = 0; virtual bool StartCursor() = 0; virtual bool ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete) = 0; @@ -166,6 +167,7 @@ class DummyBatch : public DatabaseBatch bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite=true) override { return true; } bool EraseKey(CDataStream&& key) override { return true; } bool HasKey(CDataStream&& key) override { return true; } + bool ErasePrefix(Span prefix) override { return true; } public: void Flush() override {} diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index e1a3970cd738..27710ab318f2 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -257,9 +257,11 @@ class WalletImpl : public Wallet return m_wallet->GetAddressReceiveRequests(); } bool setAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& value) override { + // Note: This method handles both saving and erasing requests based on whether value is empty. LOCK(m_wallet->cs_wallet); 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 lockCoin(const COutPoint& output, const bool write_to_db) override { diff --git a/src/wallet/sqlite.cpp b/src/wallet/sqlite.cpp index fc0fe8ff9e67..cd3db76995df 100644 --- a/src/wallet/sqlite.cpp +++ b/src/wallet/sqlite.cpp @@ -147,6 +147,7 @@ void SQLiteBatch::SetupSQLStatements() {&m_overwrite_stmt, "INSERT or REPLACE into main values(?, ?)"}, {&m_delete_stmt, "DELETE FROM main WHERE key = ?"}, {&m_cursor_stmt, "SELECT key, value FROM main"}, + {&m_delete_prefix_stmt, "DELETE FROM main WHERE instr(key, ?) = 1"}, }; for (const auto& [stmt_prepared, stmt_text] : statements) { @@ -403,6 +404,7 @@ void SQLiteBatch::Close() {&m_overwrite_stmt, "overwrite"}, {&m_delete_stmt, "delete"}, {&m_cursor_stmt, "cursor"}, + {&m_delete_prefix_stmt, "delete prefix"}, }; for (const auto& [stmt_prepared, stmt_description] : statements) { @@ -468,24 +470,34 @@ bool SQLiteBatch::WriteKey(CDataStream&& key, CDataStream&& value, bool overwrit return res == SQLITE_DONE; } -bool SQLiteBatch::EraseKey(CDataStream&& key) +bool SQLiteBatch::ExecStatement(sqlite3_stmt* stmt, Span blob) { if (!m_database.m_db) return false; - assert(m_delete_stmt); + assert(stmt); // 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 - int res = sqlite3_step(m_delete_stmt); - sqlite3_clear_bindings(m_delete_stmt); - sqlite3_reset(m_delete_stmt); + int res = sqlite3_step(stmt); + sqlite3_clear_bindings(stmt); + sqlite3_reset(stmt); if (res != SQLITE_DONE) { LogPrintf("%s: Unable to execute statement: %s\n", __func__, sqlite3_errstr(res)); } return res == SQLITE_DONE; } +bool SQLiteBatch::EraseKey(CDataStream&& key) +{ + return ExecStatement(m_delete_stmt, key); +} + +bool SQLiteBatch::ErasePrefix(Span prefix) +{ + return ExecStatement(m_delete_prefix_stmt, prefix); +} + bool SQLiteBatch::HasKey(CDataStream&& key) { if (!m_database.m_db) return false; diff --git a/src/wallet/sqlite.h b/src/wallet/sqlite.h index 47b7ebb0ecde..3de49a0f20a9 100644 --- a/src/wallet/sqlite.h +++ b/src/wallet/sqlite.h @@ -27,13 +27,16 @@ class SQLiteBatch : public DatabaseBatch sqlite3_stmt* m_overwrite_stmt{nullptr}; sqlite3_stmt* m_delete_stmt{nullptr}; sqlite3_stmt* m_cursor_stmt{nullptr}; + sqlite3_stmt* m_delete_prefix_stmt{nullptr}; void SetupSQLStatements(); + bool ExecStatement(sqlite3_stmt* stmt, Span blob); bool ReadKey(CDataStream&& key, CDataStream& value) override; bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite = true) override; bool EraseKey(CDataStream&& key) override; bool HasKey(CDataStream&& key) override; + bool ErasePrefix(Span prefix) override; public: explicit SQLiteBatch(SQLiteDatabase& database); diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index bd73767e7405..b9461f716396 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -441,19 +441,63 @@ BOOST_AUTO_TEST_CASE(ComputeTimeSmart) 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 + DatabaseFormat::SQLITE, +#endif +#ifdef USE_BDB + DatabaseFormat::BERKELEY, +#endif +}; + +void TestLoadWallet(const std::string& name, DatabaseFormat format, std::function)> f) { - CTxDestination dest = PKHash(); - LOCK(m_wallet.cs_wallet); - WalletBatch batch{m_wallet.GetDatabase()}; - m_wallet.SetAddressUsed(batch, dest, true); - m_wallet.SetAddressReceiveRequest(batch, dest, "0", "val_rr0"); - m_wallet.SetAddressReceiveRequest(batch, dest, "1", "val_rr1"); - - auto values = m_wallet.GetAddressReceiveRequests(); - BOOST_CHECK_EQUAL(values.size(), 2U); - BOOST_CHECK_EQUAL(values[0], "val_rr0"); - BOOST_CHECK_EQUAL(values[1], "val_rr1"); + node::NodeContext node; + auto chain{interfaces::MakeChain(node)}; + DatabaseOptions options; + options.require_format = format; + DatabaseStatus status; + bilingual_str error; + std::vector warnings; + auto database{MakeWalletDatabase(name, options, status, error)}; + auto wallet{std::make_shared(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 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 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 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), diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 6979c5d49fae..d6d65d8d5f41 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -876,11 +876,11 @@ void CWallet::SetSpentKeyState(WalletBatch& batch, const uint256& hash, unsigned CTxDestination dst; if (ExtractDestination(srctx->tx->vout[n].scriptPubKey, dst)) { if (IsMine(dst)) { - if (used != IsAddressUsed(dst)) { + if (used != IsAddressPreviouslySpent(dst)) { if (used) { tx_destinations.insert(dst); } - SetAddressUsed(batch, dst, used); + SetAddressPreviouslySpent(batch, dst, used); } } } @@ -893,14 +893,14 @@ bool CWallet::IsSpentKey(const CScript& scriptPubKey) const if (!ExtractDestination(scriptPubKey, dest)) { return false; } - if (IsAddressUsed(dest)) { + if (IsAddressPreviouslySpent(dest)) { return true; } if (IsLegacy()) { LegacyScriptPubKeyMan* spk_man = GetLegacyScriptPubKeyMan(); assert(spk_man != nullptr); for (const auto& keyid : GetAffectedKeys(scriptPubKey, *spk_man)) { - if (IsAddressUsed(PKHash(keyid))) { + if (IsAddressPreviouslySpent(PKHash(keyid))) { return true; } } @@ -2278,19 +2278,15 @@ bool CWallet::DelAddressBook(const CTxDestination& address) WalletBatch batch(GetDatabase()); { 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) - // NOTE: This isn't a problem for sending addresses because they never have any DestData yet! - // When adding new DestData, it should be considered here whether to retain or delete it (or move it?). + // 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 don't have any data that needs to be kept. + // When adding new address data, it should be considered here whether to retain or delete it. if (IsMine(address)) { WalletLogPrintf("%s called with IsMine address, NOT SUPPORTED. Please report this bug! %s\n", __func__, PACKAGE_BUGREPORT); return false; } - // Delete destdata tuples associated with address - std::string strAddress = EncodeDestination(address); - for (const std::pair &item : m_address_book[address].destdata) - { - batch.EraseDestData(strAddress, item.first); - } + // Delete data rows associated with this address + batch.EraseAddressData(address); m_address_book.erase(address); is_mine = IsMine(address) != ISMINE_NO; } @@ -2689,51 +2685,42 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx, bool rescanning_old 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(&dest)) return false; if (!used) { - if (auto* data = util::FindKey(m_address_book, dest)) data->destdata.erase(key); - return batch.EraseDestData(EncodeDestination(dest), key); + if (auto* data{util::FindKey(m_address_book, dest)}) data->previously_spent = false; + return batch.WriteAddressPreviouslySpent(dest, false); } - const std::string value{"1"}; - m_address_book[dest].destdata.insert(std::make_pair(key, value)); - return batch.WriteDestData(EncodeDestination(dest), key, value); + LoadAddressPreviouslySpent(dest); + return batch.WriteAddressPreviouslySpent(dest, true); } -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"}; - std::map::const_iterator i = m_address_book.find(dest); - if(i != m_address_book.end()) - { - CAddressBookData::StringMap::const_iterator j = i->second.destdata.find(key); - if(j != i->second.destdata.end()) - { - return true; - } - } + m_address_book[dest].receive_requests[id] = request; +} + +bool CWallet::IsAddressPreviouslySpent(const CTxDestination& dest) const +{ + if (auto* data{util::FindKey(m_address_book, dest)}) return data->previously_spent; return false; } std::vector CWallet::GetAddressReceiveRequests() const { - const std::string prefix{"rr"}; std::vector values; - for (const auto& address : m_address_book) { - for (const auto& data : address.second.destdata) { - if (!data.first.compare(0, prefix.size(), prefix)) { - values.emplace_back(data.second); - } + for (const auto& [dest, entry] : m_address_book) { + for (const auto& [id, request] : entry.receive_requests) { + values.emplace_back(request); } } return values; @@ -2741,15 +2728,15 @@ std::vector CWallet::GetAddressReceiveRequests() const 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 - CAddressBookData& data = m_address_book.at(dest); - if (value.empty()) { - if (!batch.EraseDestData(EncodeDestination(dest), key)) return false; - data.destdata.erase(key); - } else { - if (!batch.WriteDestData(EncodeDestination(dest), key, value)) return false; - data.destdata[key] = value; - } + if (!batch.WriteAddressReceiveRequest(dest, id, value)) return false; + m_address_book[dest].receive_requests[id] = value; + return true; +} + +bool CWallet::EraseAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id) +{ + if (!batch.EraseAddressReceiveRequest(dest, id)) return false; + m_address_book[dest].receive_requests.erase(id); return true; } diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 10a363c62fb7..b52fd952521f 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -221,8 +221,23 @@ class CAddressBookData CAddressBookData() : purpose("unknown") {} - typedef std::map StringMap; - StringMap destdata; + /** + * 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 + * that has already received funds and spent them. This is used during coin + * selection to increase privacy by not creating different transactions + * that spend from the same addresses. + */ + bool previously_spent{false}; + + /** + * 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 receive_requests{}; bool IsChange() const { return m_change; } const std::string& GetLabel() const { return m_label; } @@ -585,8 +600,10 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati 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 - void LoadDestData(const CTxDestination& dest, const std::string& key, const std::string& value) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + //! Marks destination as previously spent. + 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(). int64_t nRelockTime GUARDED_BY(cs_wallet){0}; @@ -811,11 +828,12 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati bool DelAddressBook(const CTxDestination& address); - bool IsAddressUsed(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - bool SetAddressUsed(WalletBatch& batch, const CTxDestination& dest, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool IsAddressPreviouslySpent(const CTxDestination& dest) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool SetAddressPreviouslySpent(WalletBatch& batch, const CTxDestination& dest, bool used) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::vector 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 EraseAddressReceiveRequest(WalletBatch& batch, const CTxDestination& dest, const std::string& id) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); unsigned int GetKeyPoolSize() const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index d7e4b13c5447..711b6641c709 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -568,7 +568,20 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, ssKey >> strAddress; ssKey >> strKey; 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 || strType == DBKeys::CRYPTED_HDCHAIN) { CHDChain chain; ssValue >> chain; @@ -1090,16 +1103,28 @@ void MaybeCompactWalletDB(WalletContext& context) 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) +{ + 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::WriteAddressReceiveRequest(const CTxDestination& dest, const std::string& id, const std::string& receive_request) { - return WriteIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(address, key)), value); + return WriteIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), "rr" + id)), receive_request); } -bool WalletBatch::EraseDestData(const std::string &address, const std::string &key) +bool WalletBatch::EraseAddressReceiveRequest(const CTxDestination& dest, const std::string& id) { - return EraseIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(address, key))); + return EraseIC(std::make_pair(DBKeys::DESTDATA, std::make_pair(EncodeDestination(dest), "rr" + id))); } +bool WalletBatch::EraseAddressData(const CTxDestination& dest) +{ + CDataStream prefix(SER_DISK, CLIENT_VERSION); + prefix << DBKeys::DESTDATA << EncodeDestination(dest); + return m_batch->ErasePrefix(prefix); +} bool WalletBatch::WriteHDChain(const CHDChain& chain) { if (chain.IsCrypted()) { diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 43cd4885acc5..8cfd8c58fb62 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -7,6 +7,7 @@ #define BITCOIN_WALLET_WALLETDB_H #include