Skip to content

Commit c16e909

Browse files
committed
Merge bitcoin/bitcoin#28574: wallet: optimize migration process, batch db transactions
c98fc36 wallet: migration, consolidate external wallets db writes (furszy) 7c9076a wallet: migration, consolidate main wallet db writes (furszy) 9ef20e8 wallet: provide WalletBatch to 'SetupDescriptorScriptPubKeyMans' (furszy) 34bf079 wallet: refactor ApplyMigrationData to return util::Result<void> (furszy) aacaaaa wallet: provide WalletBatch to 'RemoveTxs' (furszy) 57249ff wallet: introduce active db txn listeners (furszy) 91e065e wallet: remove post-migration signals connection (furszy) 055c053 wallet: provide WalletBatch to 'DeleteRecords' (furszy) 122d103 wallet: introduce 'SetWalletFlagWithDB' (furszy) 6052c78 wallet: decouple default descriptors creation from external signer setup (furszy) f2541d0 wallet: batch MigrateToDescriptor() db transactions (furszy) 66c9936 bench: add coverage for wallet migration process (furszy) Pull request description: Last step in a chain of PRs (#26836, #28894, #28987, #29403). #### Detailed Description: The current wallet migration process performs only individual db writes. Accessing disk to delete all legacy records, clone and clean each address book entry for every created wallet, create each new descriptor (with their corresponding master key, caches and key pool), and also clone and delete each transaction that requires to be transferred to a different wallet. This work consolidates all individual disk writes into two batch operations. One for the descriptors creation from the legacy data and a second one for the execution of the migration process itself. Efficiently dumping all the information to disk at once atomically at the end of each process. This represent a speed up and also a consistency improvement. During migration, we either want to succeed or fail. No other outcomes should be accepted. We should never leave a partially migrated wallet on disk and request the user to manually restore the previous wallet from a backup (at least not if we can avoid it). Since the speedup depends on the storage device, benchmark results can vary significantly. Locally, I have seen a 15% speedup on a USB 3.2 pendrive. #### Note for Testers: The first commit introduces a benchmark for the migration process. This one can be cherry-picked on top of master to compare results pre and post changes. Please note that the benchmark setup may take some time (~70 seconds here) due to the absence of a batching mechanism for the address generation process (`GetNewDestination()` calls). ACKs for top commit: achow101: ACK c98fc36 theStack: re-ACK c98fc36 pablomartin4btc: re-ACK c98fc36 Tree-SHA512: a52d5f2eef27811045d613637c0a9d0b7e180256ddc1c893749d98ba2882b570c45f28cc7263cadd4710f2c10db1bea33d88051f29c6b789bc6180c85b5fd8f6
2 parents dd92911 + c98fc36 commit c16e909

File tree

14 files changed

+259
-117
lines changed

14 files changed

+259
-117
lines changed

src/bench/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ if(ENABLE_WALLET)
7171
wallet_create_tx.cpp
7272
wallet_loading.cpp
7373
wallet_ismine.cpp
74+
wallet_migration.cpp
7475
)
7576
target_link_libraries(bench_bitcoin bitcoin_wallet)
7677
endif()

src/bench/wallet_migration.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) 2024 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or https://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <bitcoin-build-config.h> // IWYU pragma: keep
6+
7+
#include <bench/bench.h>
8+
#include <interfaces/chain.h>
9+
#include <node/context.h>
10+
#include <test/util/mining.h>
11+
#include <test/util/setup_common.h>
12+
#include <wallet/test/util.h>
13+
#include <wallet/context.h>
14+
#include <wallet/receive.h>
15+
#include <wallet/wallet.h>
16+
17+
#include <optional>
18+
19+
#if defined(USE_BDB) && defined(USE_SQLITE) // only enable benchmark when bdb and sqlite are enabled
20+
21+
namespace wallet{
22+
23+
static void WalletMigration(benchmark::Bench& bench)
24+
{
25+
const auto test_setup = MakeNoLogFileContext<TestingSetup>();
26+
27+
WalletContext context;
28+
context.args = &test_setup->m_args;
29+
context.chain = test_setup->m_node.chain.get();
30+
31+
// Number of imported watch only addresses
32+
int NUM_WATCH_ONLY_ADDR = 20;
33+
34+
// Setup legacy wallet
35+
DatabaseOptions options;
36+
options.use_unsafe_sync = true;
37+
options.verify = false;
38+
DatabaseStatus status;
39+
bilingual_str error;
40+
auto database = MakeWalletDatabase(fs::PathToString(test_setup->m_path_root / "legacy"), options, status, error);
41+
uint64_t create_flags = 0;
42+
auto wallet = TestLoadWallet(std::move(database), context, create_flags);
43+
44+
// Add watch-only addresses
45+
std::vector<CScript> scripts_watch_only;
46+
for (int w = 0; w < NUM_WATCH_ONLY_ADDR; ++w) {
47+
CKey key = GenerateRandomKey();
48+
LOCK(wallet->cs_wallet);
49+
const CScript& script = scripts_watch_only.emplace_back(GetScriptForDestination(GetDestinationForKey(key.GetPubKey(), OutputType::LEGACY)));
50+
bool res = wallet->ImportScriptPubKeys(strprintf("watch_%d", w), {script},
51+
/*have_solving_data=*/false, /*apply_label=*/true, /*timestamp=*/1);
52+
assert(res);
53+
}
54+
55+
// Generate transactions and local addresses
56+
for (int j = 0; j < 400; ++j) {
57+
CMutableTransaction mtx;
58+
mtx.vout.emplace_back(COIN, GetScriptForDestination(*Assert(wallet->GetNewDestination(OutputType::BECH32, strprintf("bench_%d", j)))));
59+
mtx.vout.emplace_back(COIN, GetScriptForDestination(*Assert(wallet->GetNewDestination(OutputType::LEGACY, strprintf("legacy_%d", j)))));
60+
mtx.vout.emplace_back(COIN, scripts_watch_only.at(j % NUM_WATCH_ONLY_ADDR));
61+
mtx.vin.resize(2);
62+
wallet->AddToWallet(MakeTransactionRef(mtx), TxStateInactive{}, /*update_wtx=*/nullptr, /*fFlushOnClose=*/false, /*rescanning_old_block=*/true);
63+
}
64+
65+
// Unload so the migration process loads it
66+
TestUnloadWallet(std::move(wallet));
67+
68+
bench.epochs(/*numEpochs=*/1).run([&] {
69+
util::Result<MigrationResult> res = MigrateLegacyToDescriptor(fs::PathToString(test_setup->m_path_root / "legacy"), "", context);
70+
assert(res);
71+
assert(res->wallet);
72+
assert(res->watchonly_wallet);
73+
});
74+
}
75+
76+
BENCHMARK(WalletMigration, benchmark::PriorityLevel::LOW);
77+
78+
} // namespace wallet
79+
80+
#endif // end USE_SQLITE && USE_BDB

src/wallet/bdb.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class BerkeleyBatch : public DatabaseBatch
208208
bool TxnBegin() override;
209209
bool TxnCommit() override;
210210
bool TxnAbort() override;
211+
bool HasActiveTxn() override { return activeTxn != nullptr; }
211212
DbTxn* txn() const { return activeTxn; }
212213
};
213214

src/wallet/db.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class DatabaseBatch
122122
virtual bool TxnBegin() = 0;
123123
virtual bool TxnCommit() = 0;
124124
virtual bool TxnAbort() = 0;
125+
virtual bool HasActiveTxn() = 0;
125126
};
126127

127128
/** An instance of this class represents one database.

src/wallet/migrate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class BerkeleyROBatch : public DatabaseBatch
115115
bool TxnBegin() override { return false; }
116116
bool TxnCommit() override { return false; }
117117
bool TxnAbort() override { return false; }
118+
bool HasActiveTxn() override { return false; }
118119
};
119120

120121
//! Return object giving access to Berkeley Read Only database at specified path.

src/wallet/salvage.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class DummyBatch : public DatabaseBatch
4444
bool TxnBegin() override { return true; }
4545
bool TxnCommit() override { return true; }
4646
bool TxnAbort() override { return true; }
47+
bool HasActiveTxn() override { return false; }
4748
};
4849

4950
/** A dummy WalletDatabase that does nothing and never fails. Only used by salvage.

src/wallet/scriptpubkeyman.cpp

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,12 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
18071807
keyid_it++;
18081808
}
18091809

1810+
WalletBatch batch(m_storage.GetDatabase());
1811+
if (!batch.TxnBegin()) {
1812+
LogPrintf("Error generating descriptors for migration, cannot initialize db transaction\n");
1813+
return std::nullopt;
1814+
}
1815+
18101816
// keyids is now all non-HD keys. Each key will have its own combo descriptor
18111817
for (const CKeyID& keyid : keyids) {
18121818
CKey key;
@@ -1837,8 +1843,8 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
18371843

18381844
// Make the DescriptorScriptPubKeyMan and get the scriptPubKeys
18391845
auto desc_spk_man = std::make_unique<DescriptorScriptPubKeyMan>(m_storage, w_desc, /*keypool_size=*/0);
1840-
desc_spk_man->AddDescriptorKey(key, key.GetPubKey());
1841-
desc_spk_man->TopUp();
1846+
WITH_LOCK(desc_spk_man->cs_desc_man, desc_spk_man->AddDescriptorKeyWithDB(batch, key, key.GetPubKey()));
1847+
desc_spk_man->TopUpWithDB(batch);
18421848
auto desc_spks = desc_spk_man->GetScriptPubKeys();
18431849

18441850
// Remove the scriptPubKeys from our current set
@@ -1883,8 +1889,8 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
18831889

18841890
// Make the DescriptorScriptPubKeyMan and get the scriptPubKeys
18851891
auto desc_spk_man = std::make_unique<DescriptorScriptPubKeyMan>(m_storage, w_desc, /*keypool_size=*/0);
1886-
desc_spk_man->AddDescriptorKey(master_key.key, master_key.key.GetPubKey());
1887-
desc_spk_man->TopUp();
1892+
WITH_LOCK(desc_spk_man->cs_desc_man, desc_spk_man->AddDescriptorKeyWithDB(batch, master_key.key, master_key.key.GetPubKey()));
1893+
desc_spk_man->TopUpWithDB(batch);
18881894
auto desc_spks = desc_spk_man->GetScriptPubKeys();
18891895

18901896
// Remove the scriptPubKeys from our current set
@@ -1950,9 +1956,9 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
19501956
if (!GetKey(keyid, key)) {
19511957
continue;
19521958
}
1953-
desc_spk_man->AddDescriptorKey(key, key.GetPubKey());
1959+
WITH_LOCK(desc_spk_man->cs_desc_man, desc_spk_man->AddDescriptorKeyWithDB(batch, key, key.GetPubKey()));
19541960
}
1955-
desc_spk_man->TopUp();
1961+
desc_spk_man->TopUpWithDB(batch);
19561962
auto desc_spks_set = desc_spk_man->GetScriptPubKeys();
19571963
desc_spks.insert(desc_spks.end(), desc_spks_set.begin(), desc_spks_set.end());
19581964

@@ -2019,13 +2025,26 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
20192025

20202026
// Make sure that we have accounted for all scriptPubKeys
20212027
assert(spks.size() == 0);
2028+
2029+
// Finalize transaction
2030+
if (!batch.TxnCommit()) {
2031+
LogPrintf("Error generating descriptors for migration, cannot commit db transaction\n");
2032+
return std::nullopt;
2033+
}
2034+
20222035
return out;
20232036
}
20242037

20252038
bool LegacyDataSPKM::DeleteRecords()
2039+
{
2040+
return RunWithinTxn(m_storage.GetDatabase(), /*process_desc=*/"delete legacy records", [&](WalletBatch& batch){
2041+
return DeleteRecordsWithDB(batch);
2042+
});
2043+
}
2044+
2045+
bool LegacyDataSPKM::DeleteRecordsWithDB(WalletBatch& batch)
20262046
{
20272047
LOCK(cs_KeyStore);
2028-
WalletBatch batch(m_storage.GetDatabase());
20292048
return batch.EraseRecords(DBKeys::LEGACY_TYPES);
20302049
}
20312050

src/wallet/scriptpubkeyman.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,9 @@ class LegacyDataSPKM : public ScriptPubKeyMan, public FillableSigningProvider
366366
/** Get the DescriptorScriptPubKeyMans (with private keys) that have the same scriptPubKeys as this LegacyScriptPubKeyMan.
367367
* Does not modify this ScriptPubKeyMan. */
368368
std::optional<MigrationData> MigrateToDescriptor();
369-
/** Delete all the records ofthis LegacyScriptPubKeyMan from disk*/
369+
/** Delete all the records of this LegacyScriptPubKeyMan from disk*/
370370
bool DeleteRecords();
371+
bool DeleteRecordsWithDB(WalletBatch& batch);
371372
};
372373

373374
// Implements the full legacy wallet behavior
@@ -582,6 +583,7 @@ class LegacySigningProvider : public SigningProvider
582583

583584
class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
584585
{
586+
friend class LegacyDataSPKM;
585587
private:
586588
using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
587589
using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index

src/wallet/sqlite.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class SQLiteBatch : public DatabaseBatch
9595
bool TxnBegin() override;
9696
bool TxnCommit() override;
9797
bool TxnAbort() override;
98+
bool HasActiveTxn() override { return m_txn; }
9899
};
99100

100101
/** An instance of this class represents one SQLite3 database.

src/wallet/test/util.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class MockableBatch : public DatabaseBatch
9595
bool TxnBegin() override { return m_pass; }
9696
bool TxnCommit() override { return m_pass; }
9797
bool TxnAbort() override { return m_pass; }
98+
bool HasActiveTxn() override { return false; }
9899
};
99100

100101
/** A WalletDatabase whose contents and return values can be modified as needed for testing

0 commit comments

Comments
 (0)