Skip to content

Commit 539023a

Browse files
author
MarcoFalke
committed
Merge bitcoin#22492: wallet: Reorder locks in dumpwallet to avoid lock order assertion
9b85a5e tests: Test for dumpwallet lock order issue (Andrew Chow) 25d99e6 Reorder dumpwallet so that cs_main functions go first (Andrew Chow) Pull request description: When a wallet is loaded which has an unconfirmed transaction in the mempool, it will end up establishing the lock order of cs_wallet -> cs_main -> cs_KeyStore. If `dumpwallet` is used on this wallet, then a lock order of cs_wallet -> cs_KeyStore -> cs_main will be used, which causes a lock order assertion. This PR fixes this by reordering `dumpwallet` and `GetKeyBirthTimes` (only used by `dumpwallet`). Specifically, in both functions, the function calls which lock cs_main are done prior to locking cs_KeyStore. This avoids the lock order issue. Additionally, I have added a test case to `wallet_dump.py`. Of course testing this requires `--enable-debug`. Fixes bitcoin#22489 ACKs for top commit: MarcoFalke: review ACK 9b85a5e 🎰 ryanofsky: Code review ACK 9b85a5e. Nice to reduce lock scope, and good test! prayank23: tACK bitcoin@9b85a5e lsilva01: Tested ACK bitcoin@9b85a5e under the same conditions reported in issue bitcoin#22489 and the `dumpwallet` command completed successfully. Tree-SHA512: d370a8f415ad64ee6a538ff419155837bcdbb167e3831b06572562289239028c6b46d80b23d227286afe875d9351f3377574ed831549ea426fb926af0e19c755
2 parents 8ed8164 + 9b85a5e commit 539023a

File tree

3 files changed

+53
-35
lines changed

3 files changed

+53
-35
lines changed

src/wallet/rpcdump.cpp

+9-4
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,7 @@ RPCHelpMan dumpwallet()
740740
// the user could have gotten from another RPC command prior to now
741741
wallet.BlockUntilSyncedToCurrentChain();
742742

743-
LOCK2(wallet.cs_wallet, spk_man.cs_KeyStore);
743+
LOCK(wallet.cs_wallet);
744744

745745
EnsureWalletIsUnlocked(wallet);
746746

@@ -762,9 +762,16 @@ RPCHelpMan dumpwallet()
762762
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
763763

764764
std::map<CKeyID, int64_t> mapKeyBirth;
765-
const std::map<CKeyID, int64_t>& mapKeyPool = spk_man.GetAllReserveKeys();
766765
wallet.GetKeyBirthTimes(mapKeyBirth);
767766

767+
int64_t block_time = 0;
768+
CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), FoundBlock().time(block_time)));
769+
770+
// Note: To avoid a lock order issue, access to cs_main must be locked before cs_KeyStore.
771+
// So we do the two things in this function that lock cs_main first: GetKeyBirthTimes, and findBlock.
772+
LOCK(spk_man.cs_KeyStore);
773+
774+
const std::map<CKeyID, int64_t>& mapKeyPool = spk_man.GetAllReserveKeys();
768775
std::set<CScriptID> scripts = spk_man.GetCScripts();
769776

770777
// sort time/key pairs
@@ -779,8 +786,6 @@ RPCHelpMan dumpwallet()
779786
file << strprintf("# Wallet dump created by Bitcoin %s\n", CLIENT_BUILD);
780787
file << strprintf("# * Created on %s\n", FormatISO8601DateTime(GetTime()));
781788
file << strprintf("# * Best block at time of backup was %i (%s),\n", wallet.GetLastBlockHeight(), wallet.GetLastBlockHash().ToString());
782-
int64_t block_time = 0;
783-
CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), FoundBlock().time(block_time)));
784789
file << strprintf("# mined on %s\n", FormatISO8601DateTime(block_time));
785790
file << "\n";
786791

src/wallet/wallet.cpp

+35-31
Original file line numberDiff line numberDiff line change
@@ -2298,44 +2298,48 @@ void CWallet::GetKeyBirthTimes(std::map<CKeyID, int64_t>& mapKeyBirth) const {
22982298
AssertLockHeld(cs_wallet);
22992299
mapKeyBirth.clear();
23002300

2301-
LegacyScriptPubKeyMan* spk_man = GetLegacyScriptPubKeyMan();
2302-
assert(spk_man != nullptr);
2303-
LOCK(spk_man->cs_KeyStore);
2304-
2305-
// get birth times for keys with metadata
2306-
for (const auto& entry : spk_man->mapKeyMetadata) {
2307-
if (entry.second.nCreateTime) {
2308-
mapKeyBirth[entry.first] = entry.second.nCreateTime;
2309-
}
2310-
}
2311-
23122301
// map in which we'll infer heights of other keys
23132302
std::map<CKeyID, const CWalletTx::Confirmation*> mapKeyFirstBlock;
23142303
CWalletTx::Confirmation max_confirm;
23152304
max_confirm.block_height = GetLastBlockHeight() > 144 ? GetLastBlockHeight() - 144 : 0; // the tip can be reorganized; use a 144-block safety margin
23162305
CHECK_NONFATAL(chain().findAncestorByHeight(GetLastBlockHash(), max_confirm.block_height, FoundBlock().hash(max_confirm.hashBlock)));
2317-
for (const CKeyID &keyid : spk_man->GetKeys()) {
2318-
if (mapKeyBirth.count(keyid) == 0)
2319-
mapKeyFirstBlock[keyid] = &max_confirm;
2320-
}
23212306

2322-
// if there are no such keys, we're done
2323-
if (mapKeyFirstBlock.empty())
2324-
return;
2307+
{
2308+
LegacyScriptPubKeyMan* spk_man = GetLegacyScriptPubKeyMan();
2309+
assert(spk_man != nullptr);
2310+
LOCK(spk_man->cs_KeyStore);
2311+
2312+
// get birth times for keys with metadata
2313+
for (const auto& entry : spk_man->mapKeyMetadata) {
2314+
if (entry.second.nCreateTime) {
2315+
mapKeyBirth[entry.first] = entry.second.nCreateTime;
2316+
}
2317+
}
2318+
2319+
// Prepare to infer birth heights for keys without metadata
2320+
for (const CKeyID &keyid : spk_man->GetKeys()) {
2321+
if (mapKeyBirth.count(keyid) == 0)
2322+
mapKeyFirstBlock[keyid] = &max_confirm;
2323+
}
23252324

2326-
// find first block that affects those keys, if there are any left
2327-
for (const auto& entry : mapWallet) {
2328-
// iterate over all wallet transactions...
2329-
const CWalletTx &wtx = entry.second;
2330-
if (wtx.m_confirm.status == CWalletTx::CONFIRMED) {
2331-
// ... which are already in a block
2332-
for (const CTxOut &txout : wtx.tx->vout) {
2333-
// iterate over all their outputs
2334-
for (const auto &keyid : GetAffectedKeys(txout.scriptPubKey, *spk_man)) {
2335-
// ... and all their affected keys
2336-
auto rit = mapKeyFirstBlock.find(keyid);
2337-
if (rit != mapKeyFirstBlock.end() && wtx.m_confirm.block_height < rit->second->block_height) {
2338-
rit->second = &wtx.m_confirm;
2325+
// if there are no such keys, we're done
2326+
if (mapKeyFirstBlock.empty())
2327+
return;
2328+
2329+
// find first block that affects those keys, if there are any left
2330+
for (const auto& entry : mapWallet) {
2331+
// iterate over all wallet transactions...
2332+
const CWalletTx &wtx = entry.second;
2333+
if (wtx.m_confirm.status == CWalletTx::CONFIRMED) {
2334+
// ... which are already in a block
2335+
for (const CTxOut &txout : wtx.tx->vout) {
2336+
// iterate over all their outputs
2337+
for (const auto &keyid : GetAffectedKeys(txout.scriptPubKey, *spk_man)) {
2338+
// ... and all their affected keys
2339+
auto rit = mapKeyFirstBlock.find(keyid);
2340+
if (rit != mapKeyFirstBlock.end() && wtx.m_confirm.block_height < rit->second->block_height) {
2341+
rit->second = &wtx.m_confirm;
2342+
}
23392343
}
23402344
}
23412345
}

test/functional/wallet_dump.py

+9
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,15 @@ def run_test(self):
209209
with self.nodes[0].assert_debug_log(['Flushing wallet.dat'], timeout=20):
210210
self.nodes[0].getnewaddress()
211211

212+
# Make sure that dumpwallet doesn't have a lock order issue when there is an unconfirmed tx and it is reloaded
213+
# See https://github.com/bitcoin/bitcoin/issues/22489
214+
self.nodes[0].createwallet("w3")
215+
w3 = self.nodes[0].get_wallet_rpc("w3")
216+
w3.importprivkey(privkey=self.nodes[0].get_deterministic_priv_key().key, label="coinbase_import")
217+
w3.sendtoaddress(w3.getnewaddress(), 10)
218+
w3.unloadwallet()
219+
self.nodes[0].loadwallet("w3")
220+
w3.dumpwallet(os.path.join(self.nodes[0].datadir, "w3.dump"))
212221

213222
if __name__ == '__main__':
214223
WalletDumpTest().main()

0 commit comments

Comments
 (0)