Skip to content

Commit 3682609

Browse files
committed
walletdb: Repair corrupted doubled derivation paths
A bug in 0.21.x and 22.x resulted in some wallets having invalid derivation paths that are the concatenation of two derivation paths. This corruption follows a rigid pattern and can be easily detected and repaired.
1 parent c66c683 commit 3682609

File tree

2 files changed

+58
-23
lines changed

2 files changed

+58
-23
lines changed

src/wallet/walletdb.cpp

+57-22
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ static LoadResult LoadRecords(CWallet* pwallet, DatabaseBatch& batch, const std:
537537
return LoadRecords(pwallet, batch, key, prefix, load_func);
538538
}
539539

540-
static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch, int last_client) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
540+
static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, WalletBatch& batch, int last_client) EXCLUSIVE_LOCKS_REQUIRED(pwallet->cs_wallet)
541541
{
542542
AssertLockHeld(pwallet->cs_wallet);
543543
DBErrors result = DBErrors::LOAD_OK;
@@ -550,7 +550,7 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
550550

551551
DataStream prefix;
552552
prefix << type;
553-
std::unique_ptr<DatabaseCursor> cursor = batch.GetNewPrefixCursor(prefix);
553+
std::unique_ptr<DatabaseCursor> cursor = batch.m_batch->GetNewPrefixCursor(prefix);
554554
if (!cursor) {
555555
pwallet->WalletLogPrintf("Error getting database cursor for '%s' records\n", type);
556556
return DBErrors::CORRUPT;
@@ -568,28 +568,28 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
568568

569569
// Load HD Chain
570570
// Note: There should only be one HDCHAIN record with no data following the type
571-
LoadResult hd_chain_res = LoadRecords(pwallet, batch, DBKeys::HDCHAIN,
571+
LoadResult hd_chain_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::HDCHAIN,
572572
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
573573
return LoadHDChain(pwallet, value, err) ? DBErrors:: LOAD_OK : DBErrors::CORRUPT;
574574
});
575575
result = std::max(result, hd_chain_res.m_result);
576576

577577
// Load unencrypted keys
578-
LoadResult key_res = LoadRecords(pwallet, batch, DBKeys::KEY,
578+
LoadResult key_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::KEY,
579579
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
580580
return LoadKey(pwallet, key, value, err) ? DBErrors::LOAD_OK : DBErrors::CORRUPT;
581581
});
582582
result = std::max(result, key_res.m_result);
583583

584584
// Load encrypted keys
585-
LoadResult ckey_res = LoadRecords(pwallet, batch, DBKeys::CRYPTED_KEY,
585+
LoadResult ckey_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::CRYPTED_KEY,
586586
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
587587
return LoadCryptedKey(pwallet, key, value, err) ? DBErrors::LOAD_OK : DBErrors::CORRUPT;
588588
});
589589
result = std::max(result, ckey_res.m_result);
590590

591591
// Load scripts
592-
LoadResult script_res = LoadRecords(pwallet, batch, DBKeys::CSCRIPT,
592+
LoadResult script_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::CSCRIPT,
593593
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& strErr) {
594594
uint160 hash;
595595
key >> hash;
@@ -612,13 +612,13 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
612612

613613
// Load keymeta
614614
std::map<uint160, CHDChain> hd_chains;
615-
LoadResult keymeta_res = LoadRecords(pwallet, batch, DBKeys::KEYMETA,
616-
[&hd_chains] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& strErr) {
615+
std::vector<CPubKey> updated_metas;
616+
LoadResult keymeta_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::KEYMETA,
617+
[&hd_chains, &updated_metas] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& strErr) {
617618
CPubKey vchPubKey;
618619
key >> vchPubKey;
619620
CKeyMetadata keyMeta;
620621
value >> keyMeta;
621-
pwallet->GetOrCreateLegacyDataSPKM()->LoadKeyMetadata(vchPubKey.GetID(), keyMeta);
622622

623623
// Extract some CHDChain info from this metadata if it has any
624624
if (keyMeta.nVersion >= CKeyMetadata::VERSION_WITH_HDDATA && !keyMeta.hd_seed_id.IsNull() && keyMeta.hdKeypath.size() > 0) {
@@ -629,13 +629,32 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
629629
uint32_t index = 0;
630630
if (keyMeta.hdKeypath != "s" && keyMeta.hdKeypath != "m") {
631631
std::vector<uint32_t> path;
632-
if (keyMeta.has_key_origin) {
633-
// We have a key origin, so pull it from its path vector
634-
path = keyMeta.key_origin.path;
635-
} else {
636-
// No key origin, have to parse the string
637-
if (!ParseHDKeypath(keyMeta.hdKeypath, path)) {
638-
strErr = "Error reading wallet database: keymeta with invalid HD keypath";
632+
// Always parse the path string
633+
if (!ParseHDKeypath(keyMeta.hdKeypath, path)) {
634+
strErr = "Error reading wallet database: keymeta with invalid HD keypath";
635+
return DBErrors::NONCRITICAL_ERROR;
636+
}
637+
// If there is a key origin, make sure that path matches
638+
if (keyMeta.has_key_origin && path != keyMeta.key_origin.path) {
639+
// Detect if this metadata has a bad derivation path from a bug in inactive hd key derivation that was present in 0.21 and 22.x
640+
// See https://github.com/bitcoin/bitcoin/issues/29109
641+
// Markers for bad derivation:
642+
// - 6 indexes
643+
// - path[5] == path[2] + 1
644+
// - path[0:2] == path[3:5]
645+
// - path[3:6] matches hdKeypath
646+
if (keyMeta.key_origin.path.size() == 6
647+
&& keyMeta.key_origin.path[5] == keyMeta.key_origin.path[2] + 1
648+
&& keyMeta.key_origin.path[0] == keyMeta.key_origin.path[3]
649+
&& keyMeta.key_origin.path[1] == keyMeta.key_origin.path[4]
650+
&& std::equal(keyMeta.key_origin.path.begin() + 3, keyMeta.key_origin.path.end(), path.begin(), path.end())
651+
) {
652+
// Matches the pattern, just reset it to the parsed path
653+
pwallet->WalletLogPrintf("Repairing derivation path for %s\n", HexStr(vchPubKey));
654+
keyMeta.key_origin.path = path;
655+
updated_metas.push_back(vchPubKey);
656+
} else {
657+
strErr = "keymeta with mismatched hdkeypath string and key origin derivation path";
639658
return DBErrors::NONCRITICAL_ERROR;
640659
}
641660
}
@@ -679,10 +698,26 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
679698
chain.nExternalChainCounter = std::max(chain.nExternalChainCounter, index + 1);
680699
}
681700
}
701+
702+
pwallet->GetOrCreateLegacyDataSPKM()->LoadKeyMetadata(vchPubKey.GetID(), keyMeta);
703+
682704
return DBErrors::LOAD_OK;
683705
});
684706
result = std::max(result, keymeta_res.m_result);
685707

708+
// Update any changed metadata
709+
if (!updated_metas.empty()) {
710+
LegacyScriptPubKeyMan* spkm = pwallet->GetLegacyScriptPubKeyMan();
711+
Assert(spkm);
712+
LOCK(spkm->cs_KeyStore);
713+
for (const auto& pubkey : updated_metas) {
714+
if (!batch.WriteKeyMetadata(spkm->mapKeyMetadata.at(pubkey.GetID()), pubkey, true)) {
715+
pwallet->WalletLogPrintf("Unable to write repaired keymeta for %s\n", HexStr(pubkey));
716+
result = std::max(result, DBErrors::NONCRITICAL_ERROR);
717+
}
718+
}
719+
}
720+
686721
// Set inactive chains
687722
if (!hd_chains.empty()) {
688723
LegacyDataSPKM* legacy_spkm = pwallet->GetLegacyDataSPKM();
@@ -699,7 +734,7 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
699734
}
700735

701736
// Load watchonly scripts
702-
LoadResult watch_script_res = LoadRecords(pwallet, batch, DBKeys::WATCHS,
737+
LoadResult watch_script_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::WATCHS,
703738
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
704739
CScript script;
705740
key >> script;
@@ -713,7 +748,7 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
713748
result = std::max(result, watch_script_res.m_result);
714749

715750
// Load watchonly meta
716-
LoadResult watch_meta_res = LoadRecords(pwallet, batch, DBKeys::WATCHMETA,
751+
LoadResult watch_meta_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::WATCHMETA,
717752
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
718753
CScript script;
719754
key >> script;
@@ -725,7 +760,7 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
725760
result = std::max(result, watch_meta_res.m_result);
726761

727762
// Load keypool
728-
LoadResult pool_res = LoadRecords(pwallet, batch, DBKeys::POOL,
763+
LoadResult pool_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::POOL,
729764
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
730765
int64_t nIndex;
731766
key >> nIndex;
@@ -742,7 +777,7 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
742777
// We don't want or need the default key, but if there is one set,
743778
// we want to make sure that it is valid so that we can detect corruption
744779
// Note: There should only be one DEFAULTKEY with nothing trailing the type
745-
LoadResult default_key_res = LoadRecords(pwallet, batch, DBKeys::DEFAULTKEY,
780+
LoadResult default_key_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::DEFAULTKEY,
746781
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
747782
CPubKey default_pubkey;
748783
try {
@@ -760,7 +795,7 @@ static DBErrors LoadLegacyWalletRecords(CWallet* pwallet, DatabaseBatch& batch,
760795
result = std::max(result, default_key_res.m_result);
761796

762797
// "wkey" records are unsupported, if we see any, throw an error
763-
LoadResult wkey_res = LoadRecords(pwallet, batch, DBKeys::OLD_KEY,
798+
LoadResult wkey_res = LoadRecords(pwallet, *batch.m_batch, DBKeys::OLD_KEY,
764799
[] (CWallet* pwallet, DataStream& key, DataStream& value, std::string& err) {
765800
err = "Found unsupported 'wkey' record, try loading with version 0.18";
766801
return DBErrors::LOAD_FAIL;
@@ -1185,7 +1220,7 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
11851220
#endif
11861221

11871222
// Load legacy wallet keys
1188-
result = std::max(LoadLegacyWalletRecords(pwallet, *m_batch, last_client), result);
1223+
result = std::max(LoadLegacyWalletRecords(pwallet, *this, last_client), result);
11891224

11901225
// Load descriptors
11911226
result = std::max(LoadDescriptorWalletRecords(pwallet, *m_batch, last_client), result);

src/wallet/walletdb.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,8 @@ class WalletBatch
292292
bool TxnCommit();
293293
//! Abort current transaction
294294
bool TxnAbort();
295-
private:
296295
std::unique_ptr<DatabaseBatch> m_batch;
296+
private:
297297
WalletDatabase& m_database;
298298
};
299299

0 commit comments

Comments
 (0)